diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5d4cff6ae..89e9d33df 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -506,6 +506,58 @@ stages: testResultsFiles: '**/TestResult.xml' testRunTitle: '$(testName) Unit Tests' failTaskOnFailedTests: true + + - job: Unit_LinuxCore_Postgres + displayName: Unit Native LinuxCore with Postgres Database + dependsOn: Prepare + condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) + variables: + pattern: 'Prowlarr.*.linux-core-x64.tar.gz' + artifactName: LinuxCoreTests + Prowlarr__Postgres__Host: 'localhost' + Prowlarr__Postgres__Port: '5432' + Prowlarr__Postgres__User: 'prowlarr' + Prowlarr__Postgres__Password: 'prowlarr' + + pool: + vmImage: 'ubuntu-18.04' + + timeoutInMinutes: 10 + + steps: + - task: UseDotNet@2 + displayName: 'Install .net core' + inputs: + version: $(dotnetVersion) + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download Test Artifact + inputs: + buildType: 'current' + artifactName: $(artifactName) + targetPath: $(testsFolder) + - bash: find ${TESTSFOLDER} -name "Prowlarr.Test.Dummy" -exec chmod a+x {} \; + displayName: Make Test Dummy Executable + condition: and(succeeded(), ne(variables['osName'], 'Windows')) + - bash: | + docker run -d --name=postgres14 \ + -e POSTGRES_PASSWORD=prowlarr \ + -e POSTGRES_USER=prowlarr \ + -p 5432:5432/tcp \ + postgres:14 + displayName: Start postgres + - bash: | + chmod a+x ${TESTSFOLDER}/test.sh + ls -lR ${TESTSFOLDER} + ${TESTSFOLDER}/test.sh Linux Unit Test + displayName: Run Tests + - task: PublishTestResults@2 + displayName: Publish Test Results + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '**/TestResult.xml' + testRunTitle: 'LinuxCore Postgres Unit Tests' + failTaskOnFailedTests: true - stage: Integration displayName: Integration @@ -590,6 +642,67 @@ stages: failTaskOnFailedTests: true displayName: Publish Test Results + - job: Integration_LinuxCore_Postgres + displayName: Integration Native LinuxCore with Postgres Database + dependsOn: Prepare + condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) + variables: + pattern: 'Prowlarr.*.linux-core-x64.tar.gz' + Prowlarr__Postgres__Host: 'localhost' + Prowlarr__Postgres__Port: '5432' + Prowlarr__Postgres__User: 'prowlarr' + Prowlarr__Postgres__Password: 'prowlarr' + + pool: + vmImage: 'ubuntu-18.04' + + steps: + - task: UseDotNet@2 + displayName: 'Install .net core' + inputs: + version: $(dotnetVersion) + - checkout: none + - task: DownloadPipelineArtifact@2 + displayName: Download Test Artifact + inputs: + buildType: 'current' + artifactName: 'LinuxCoreTests' + targetPath: $(testsFolder) + - task: DownloadPipelineArtifact@2 + displayName: Download Build Artifact + inputs: + buildType: 'current' + artifactName: Packages + itemPattern: '**/$(pattern)' + targetPath: $(Build.ArtifactStagingDirectory) + - task: ExtractFiles@1 + inputs: + archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)' + destinationFolder: '$(Build.ArtifactStagingDirectory)/bin' + displayName: Extract Package + - bash: | + mkdir -p ./bin/ + cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Prowlarr/. ./bin/ + displayName: Move Package Contents + - bash: | + docker run -d --name=postgres14 \ + -e POSTGRES_PASSWORD=prowlarr \ + -e POSTGRES_USER=prowlarr \ + -p 5432:5432/tcp \ + postgres:14 + displayName: Start postgres + - bash: | + chmod a+x ${TESTSFOLDER}/test.sh + ${TESTSFOLDER}/test.sh Linux Integration Test + displayName: Run Integration Tests + - task: PublishTestResults@2 + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '**/TestResult.xml' + testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests' + failTaskOnFailedTests: true + displayName: Publish Test Results + - job: Integration_FreeBSD displayName: Integration Native FreeBSD dependsOn: Prepare diff --git a/src/NzbDrone.Automation.Test/AutomationTest.cs b/src/NzbDrone.Automation.Test/AutomationTest.cs index 638b1aaf5..840346de8 100644 --- a/src/NzbDrone.Automation.Test/AutomationTest.cs +++ b/src/NzbDrone.Automation.Test/AutomationTest.cs @@ -44,7 +44,7 @@ namespace NzbDrone.Automation.Test driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080); - _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger()); + _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null); _runner.KillAll(); _runner.Start(); diff --git a/src/NzbDrone.Common.Test/ProcessProviderFixture.cs b/src/NzbDrone.Common.Test/ProcessProviderFixture.cs index b81bbfa19..106c25fa0 100644 --- a/src/NzbDrone.Common.Test/ProcessProviderFixture.cs +++ b/src/NzbDrone.Common.Test/ProcessProviderFixture.cs @@ -170,7 +170,7 @@ namespace NzbDrone.Common.Test var processStarted = new ManualResetEventSlim(); string suffix; - if (OsInfo.IsWindows || PlatformInfo.IsMono) + if (OsInfo.IsWindows) { suffix = ".exe"; } diff --git a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs index 560bf39ea..9c348316f 100644 --- a/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs +++ b/src/NzbDrone.Common.Test/ServiceFactoryFixture.cs @@ -4,11 +4,13 @@ using DryIoc.Microsoft.DependencyInjection; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Events; @@ -29,7 +31,8 @@ namespace NzbDrone.Common.Test .AddDummyDatabase() .AddStartupContext(new StartupContext("first", "second")); - container.RegisterInstance(new Mock().Object); + container.RegisterInstance(new Mock().Object); + container.RegisterInstance(new Mock>().Object); var serviceProvider = container.GetServiceProvider(); diff --git a/src/NzbDrone.Common/Processes/ProcessProvider.cs b/src/NzbDrone.Common/Processes/ProcessProvider.cs index 953450aaa..796f4f6e8 100644 --- a/src/NzbDrone.Common/Processes/ProcessProvider.cs +++ b/src/NzbDrone.Common/Processes/ProcessProvider.cs @@ -127,7 +127,18 @@ namespace NzbDrone.Common.Processes try { _logger.Trace("Setting environment variable '{0}' to '{1}'", environmentVariable.Key, environmentVariable.Value); - startInfo.EnvironmentVariables.Add(environmentVariable.Key.ToString(), environmentVariable.Value.ToString()); + + var key = environmentVariable.Key.ToString(); + var value = environmentVariable.Value?.ToString(); + + if (startInfo.EnvironmentVariables.ContainsKey(key)) + { + startInfo.EnvironmentVariables[key] = value; + } + else + { + startInfo.EnvironmentVariables.Add(key, value); + } } catch (Exception e) { diff --git a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs index 1bc182395..985fbb7d5 100644 --- a/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs +++ b/src/NzbDrone.Core.Test/Datastore/DatabaseFixture.cs @@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Datastore public void SingleOrDefault_should_return_null_on_empty_db() { Mocker.Resolve() - .OpenConnection().Query("SELECT * FROM Indexers") + .OpenConnection().Query("SELECT * FROM \"Indexers\"") .SingleOrDefault() .Should() .BeNull(); diff --git a/src/NzbDrone.Core.Test/Framework/DbTest.cs b/src/NzbDrone.Core.Test/Framework/DbTest.cs index e38336375..05cfd5bfd 100644 --- a/src/NzbDrone.Core.Test/Framework/DbTest.cs +++ b/src/NzbDrone.Core.Test/Framework/DbTest.cs @@ -5,10 +5,14 @@ using System.IO; using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Npgsql; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Test.Common.Datastore; namespace NzbDrone.Core.Test.Framework { @@ -49,6 +53,7 @@ namespace NzbDrone.Core.Test.Framework public abstract class DbTest : CoreTest { private ITestDatabase _db; + private DatabaseType _databaseType; protected virtual MigrationType MigrationType => MigrationType.Main; @@ -101,17 +106,39 @@ namespace NzbDrone.Core.Test.Framework private IDatabase CreateDatabase(MigrationContext migrationContext) { + if (_databaseType == DatabaseType.PostgreSQL) + { + CreatePostgresDb(); + } + var factory = Mocker.Resolve(); // If a special migration test or log migration then create new - if (migrationContext.BeforeMigration != null) + if (migrationContext.BeforeMigration != null || _databaseType == DatabaseType.PostgreSQL) { return factory.Create(migrationContext); } + return CreateSqliteDatabase(factory, migrationContext); + } + + private void CreatePostgresDb() + { + var options = Mocker.Resolve>().Value; + PostgresDatabase.Create(options, MigrationType); + } + + private void DropPostgresDb() + { + var options = Mocker.Resolve>().Value; + PostgresDatabase.Drop(options, MigrationType); + } + + private IDatabase CreateSqliteDatabase(IDbFactory factory, MigrationContext migrationContext) + { // Otherwise try to use a cached migrated db - var cachedDb = GetCachedDatabase(migrationContext.MigrationType); - var testDb = GetTestDb(migrationContext.MigrationType); + var cachedDb = SqliteDatabase.GetCachedDb(migrationContext.MigrationType); + var testDb = GetTestSqliteDb(migrationContext.MigrationType); if (File.Exists(cachedDb)) { TestLogger.Info($"Using cached initial database {cachedDb}"); @@ -131,12 +158,7 @@ namespace NzbDrone.Core.Test.Framework } } - private string GetCachedDatabase(MigrationType type) - { - return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db"); - } - - private string GetTestDb(MigrationType type) + private string GetTestSqliteDb(MigrationType type) { return type == MigrationType.Main ? TestFolderInfo.GetDatabase() : TestFolderInfo.GetLogDatabase(); } @@ -151,6 +173,13 @@ namespace NzbDrone.Core.Test.Framework WithTempAsAppPath(); SetupLogging(); + // populate the possible postgres options + var postgresOptions = PostgresDatabase.GetTestOptions(); + _databaseType = postgresOptions.Host.IsNotNullOrWhiteSpace() ? DatabaseType.PostgreSQL : DatabaseType.SQLite; + + // Set up remaining container services + Mocker.SetConstant(Options.Create(postgresOptions)); + Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); Mocker.SetConstant(Mocker.Resolve()); @@ -171,11 +200,17 @@ namespace NzbDrone.Core.Test.Framework GC.Collect(); GC.WaitForPendingFinalizers(); SQLiteConnection.ClearAllPools(); + NpgsqlConnection.ClearAllPools(); if (TestFolderInfo != null) { DeleteTempFolder(TestFolderInfo.AppDataFolder); } + + if (_databaseType == DatabaseType.PostgreSQL) + { + DropPostgresDb(); + } } } } diff --git a/src/NzbDrone.Core.Test/Framework/DbTestCleanup.cs b/src/NzbDrone.Core.Test/Framework/DbTestCleanup.cs index 587043e95..ae6b102a7 100644 --- a/src/NzbDrone.Core.Test/Framework/DbTestCleanup.cs +++ b/src/NzbDrone.Core.Test/Framework/DbTestCleanup.cs @@ -1,5 +1,7 @@ using System.IO; using NUnit.Framework; +using NzbDrone.Core.Datastore.Migration.Framework; +using NzbDrone.Test.Common.Datastore; namespace NzbDrone.Core.Test { @@ -10,13 +12,13 @@ namespace NzbDrone.Core.Test [OneTimeTearDown] public void ClearCachedDatabase() { - var mainCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Main.db"); + var mainCache = SqliteDatabase.GetCachedDb(MigrationType.Main); if (File.Exists(mainCache)) { File.Delete(mainCache); } - var logCache = Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_Log.db"); + var logCache = SqliteDatabase.GetCachedDb(MigrationType.Log); if (File.Exists(logCache)) { File.Delete(logCache); diff --git a/src/NzbDrone.Core.Test/Framework/TestDatabase.cs b/src/NzbDrone.Core.Test/Framework/TestDatabase.cs index 3fbfdf028..5391cfb1f 100644 --- a/src/NzbDrone.Core.Test/Framework/TestDatabase.cs +++ b/src/NzbDrone.Core.Test/Framework/TestDatabase.cs @@ -23,6 +23,7 @@ namespace NzbDrone.Core.Test.Framework where T : ModelBase, new(); IDirectDataMapper GetDirectDataMapper(); IDbConnection OpenConnection(); + DatabaseType DatabaseType { get; } } public class TestDatabase : ITestDatabase @@ -30,6 +31,8 @@ namespace NzbDrone.Core.Test.Framework private readonly IDatabase _dbConnection; private readonly IEventAggregator _eventAggregator; + public DatabaseType DatabaseType => _dbConnection.DatabaseType; + public TestDatabase(IDatabase dbConnection) { _eventAggregator = new Mock().Object; diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs index 68e4c87e6..39e193368 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/FixFutureRunScheduledTasksFixture.cs @@ -41,7 +41,8 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers Subject.Clean(); - AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().Be(expectedTime)); + // BeCloseTo handles Postgres rounding times + AllStoredModels.ToList().ForEach(t => t.LastExecution.Should().BeCloseTo(expectedTime)); } } } diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs index 796d475c9..6b55b6ece 100644 --- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs +++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs @@ -5,12 +5,14 @@ using System.Linq; using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; +using Microsoft.Extensions.Options; using NzbDrone.Common.Cache; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Core.Authentication; using NzbDrone.Core.Configuration.Events; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Lifecycle; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Events; @@ -67,6 +69,7 @@ namespace NzbDrone.Core.Configuration private readonly IEventAggregator _eventAggregator; private readonly IDiskProvider _diskProvider; private readonly ICached _cache; + private readonly PostgresOptions _postgresOptions; private readonly string _configFile; private static readonly Regex HiddenCharacterRegex = new Regex("[^a-z0-9]", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -76,12 +79,14 @@ namespace NzbDrone.Core.Configuration public ConfigFileProvider(IAppFolderInfo appFolderInfo, ICacheManager cacheManager, IEventAggregator eventAggregator, - IDiskProvider diskProvider) + IDiskProvider diskProvider, + IOptions postgresOptions) { _cache = cacheManager.GetCache(GetType()); _eventAggregator = eventAggregator; _diskProvider = diskProvider; _configFile = appFolderInfo.GetConfigPath(); + _postgresOptions = postgresOptions.Value; } public Dictionary GetConfigDictionary() @@ -195,13 +200,13 @@ namespace NzbDrone.Core.Configuration public string LogLevel => GetValue("LogLevel", "info").ToLowerInvariant(); public string ConsoleLogLevel => GetValue("ConsoleLogLevel", string.Empty, persist: false); - public string PostgresHost => GetValue("PostgresHost", string.Empty, persist: false); - public string PostgresUser => GetValue("PostgresUser", string.Empty, persist: false); - public string PostgresPassword => GetValue("PostgresPassword", string.Empty, persist: false); - public string PostgresMainDb => GetValue("PostgresMainDb", "prowlarr-main", persist: false); - public string PostgresLogDb => GetValue("PostgresLogDb", "prowlarr-log", persist: false); + public string PostgresHost => _postgresOptions?.Host ?? GetValue("PostgresHost", string.Empty, persist: false); + public string PostgresUser => _postgresOptions?.User ?? GetValue("PostgresUser", string.Empty, persist: false); + public string PostgresPassword => _postgresOptions?.Password ?? GetValue("PostgresPassword", string.Empty, persist: false); + public string PostgresMainDb => _postgresOptions?.MainDb ?? GetValue("PostgresMainDb", "prowlarr-main", persist: false); + public string PostgresLogDb => _postgresOptions?.LogDb ?? GetValue("PostgresLogDb", "prowlarr-log", persist: false); + public int PostgresPort => (_postgresOptions?.Port ?? 0) != 0 ? _postgresOptions.Port : GetValueInt("PostgresPort", 5432, persist: false); public string Theme => GetValue("Theme", "light", persist: false); - public int PostgresPort => GetValueInt("PostgresPort", 5432, persist: false); public bool LogSql => GetValueBoolean("LogSql", false, persist: false); public int LogRotate => GetValueInt("LogRotate", 50, persist: false); public bool FilterSentryEvents => GetValueBoolean("FilterSentryEvents", true, persist: false); diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs index 33253efa9..b34b530b2 100644 --- a/src/NzbDrone.Core/Datastore/BasicRepository.cs +++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs @@ -167,14 +167,12 @@ namespace NzbDrone.Core.Datastore } } - if (_database.DatabaseType == DatabaseType.SQLite) - { - return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id"; - } - else + if (_database.DatabaseType == DatabaseType.PostgreSQL) { return $"INSERT INTO \"{_table}\" ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}) RETURNING \"Id\""; } + + return $"INSERT INTO {_table} ({sbColumnList.ToString()}) VALUES ({sbParameterList.ToString()}); SELECT last_insert_rowid() id"; } private TModel Insert(IDbConnection connection, IDbTransaction transaction, TModel model) diff --git a/src/NzbDrone.Core/Datastore/PostgresOptions.cs b/src/NzbDrone.Core/Datastore/PostgresOptions.cs new file mode 100644 index 000000000..b67ec66ea --- /dev/null +++ b/src/NzbDrone.Core/Datastore/PostgresOptions.cs @@ -0,0 +1,26 @@ +using Microsoft.Extensions.Configuration; + +namespace NzbDrone.Core.Datastore +{ + public class PostgresOptions + { + public string Host { get; set; } + public int Port { get; set; } + public string User { get; set; } + public string Password { get; set; } + public string MainDb { get; set; } + public string LogDb { get; set; } + + public static PostgresOptions GetOptions() + { + var config = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + + var postgresOptions = new PostgresOptions(); + config.GetSection("Prowlarr:Postgres").Bind(postgresOptions); + + return postgresOptions; + } + } +} diff --git a/src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs b/src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs index bc4c39490..f3e31e179 100644 --- a/src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs +++ b/src/NzbDrone.Core/Datastore/WhereBuilderPostgres.cs @@ -313,7 +313,20 @@ namespace NzbDrone.Core.Datastore _sb.Append(" = ANY ("); - Visit(list); + // hardcode the integer list if it exists to bypass parameter limit + if (item.Type == typeof(int) && TryGetRightValue(list, out var value)) + { + var items = (IEnumerable)value; + _sb.Append("('{"); + _sb.Append(string.Join(", ", items)); + _sb.Append("}')"); + + _gotConcreteValue = true; + } + else + { + Visit(list); + } _sb.Append("))"); } @@ -324,7 +337,7 @@ namespace NzbDrone.Core.Datastore Visit(body.Object); - _sb.Append(" LIKE '%' || "); + _sb.Append(" ILIKE '%' || "); Visit(body.Arguments[0]); @@ -337,7 +350,7 @@ namespace NzbDrone.Core.Datastore Visit(body.Object); - _sb.Append(" LIKE "); + _sb.Append(" ILIKE "); Visit(body.Arguments[0]); @@ -350,7 +363,7 @@ namespace NzbDrone.Core.Datastore Visit(body.Object); - _sb.Append(" LIKE '%' || "); + _sb.Append(" ILIKE '%' || "); Visit(body.Arguments[0]); diff --git a/src/NzbDrone.Host.Test/ContainerFixture.cs b/src/NzbDrone.Host.Test/ContainerFixture.cs index 30774afbd..4f61aa877 100644 --- a/src/NzbDrone.Host.Test/ContainerFixture.cs +++ b/src/NzbDrone.Host.Test/ContainerFixture.cs @@ -5,13 +5,16 @@ using DryIoc.Microsoft.DependencyInjection; using FluentAssertions; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using NzbDrone.Common; using NzbDrone.Common.Composition.Extensions; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Instrumentation.Extensions; +using NzbDrone.Core.Datastore; using NzbDrone.Core.Datastore.Extensions; +using NzbDrone.Core.Download; using NzbDrone.Core.Indexers; using NzbDrone.Core.Jobs; using NzbDrone.Core.Lifecycle; @@ -43,6 +46,7 @@ namespace NzbDrone.App.Test // dummy lifetime and broadcaster so tests resolve container.RegisterInstance(new Mock().Object); container.RegisterInstance(new Mock().Object); + container.RegisterInstance>(new Mock>().Object); _container = container.GetServiceProvider(); } @@ -53,6 +57,12 @@ namespace NzbDrone.App.Test _container.GetRequiredService>().Should().NotBeEmpty(); } + [Test] + public void should_be_able_to_resolve_downloadclients() + { + _container.GetRequiredService>().Should().NotBeEmpty(); + } + [Test] public void container_should_inject_itself() { diff --git a/src/NzbDrone.Host/Bootstrap.cs b/src/NzbDrone.Host/Bootstrap.cs index 42b434cef..d4ab88f39 100644 --- a/src/NzbDrone.Host/Bootstrap.cs +++ b/src/NzbDrone.Host/Bootstrap.cs @@ -9,8 +9,11 @@ using System.Security.Cryptography.X509Certificates; using System.Text; using DryIoc; using DryIoc.Microsoft.DependencyInjection; +using FluentMigrator.Runner.Processors.Postgres; +using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.WindowsServices; using NLog; @@ -23,6 +26,8 @@ using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Core.Configuration; using NzbDrone.Core.Datastore.Extensions; +using NzbDrone.Host; +using PostgresOptions = NzbDrone.Core.Datastore.PostgresOptions; namespace NzbDrone.Host { @@ -52,6 +57,7 @@ namespace NzbDrone.Host Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var appMode = GetApplicationMode(startupContext); + var config = GetConfiguration(startupContext); switch (appMode) { @@ -80,12 +86,22 @@ namespace NzbDrone.Host // Utility mode default: { - new Container(rules => rules.WithNzbDroneRules()) - .AutoAddServices(ASSEMBLIES) - .AddNzbDroneLogger() - .AddStartupContext(startupContext) - .Resolve() - .Route(appMode); + new HostBuilder() + .UseServiceProviderFactory(new DryIocServiceProviderFactory(new Container(rules => rules.WithNzbDroneRules()))) + .ConfigureContainer(c => + { + c.AutoAddServices(Bootstrap.ASSEMBLIES) + .AddNzbDroneLogger() + .AddDatabase() + .AddStartupContext(startupContext) + .Resolve() + .Route(appMode); + }) + .ConfigureServices(services => + { + services.Configure(config.GetSection("Prowlarr:Postgres")); + }).Build(); + break; } } @@ -135,6 +151,10 @@ namespace NzbDrone.Host .AddDatabase() .AddStartupContext(context); }) + .ConfigureServices(services => + { + services.Configure(config.GetSection("Prowlarr:Postgres")); + }) .ConfigureWebHost(builder => { builder.UseConfiguration(config); @@ -205,6 +225,7 @@ namespace NzbDrone.Host return new ConfigurationBuilder() .AddXmlFile(appFolder.GetConfigPath(), optional: true, reloadOnChange: false) .AddInMemoryCollection(new List> { new ("dataProtectionFolder", appFolder.GetDataProtectionPath()) }) + .AddEnvironmentVariables() .Build(); } diff --git a/src/NzbDrone.Integration.Test/IntegrationTest.cs b/src/NzbDrone.Integration.Test/IntegrationTest.cs index 3b64124b8..609117a19 100644 --- a/src/NzbDrone.Integration.Test/IntegrationTest.cs +++ b/src/NzbDrone.Integration.Test/IntegrationTest.cs @@ -1,9 +1,15 @@ +using System.Collections.Generic; using System.Threading; +using Microsoft.Extensions.Configuration; using NLog; +using Npgsql; using NUnit.Framework; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Migration.Framework; using NzbDrone.Core.Indexers.FileList; using NzbDrone.Test.Common; +using NzbDrone.Test.Common.Datastore; using Prowlarr.Http.ClientSchema; namespace NzbDrone.Integration.Test @@ -19,6 +25,8 @@ namespace NzbDrone.Integration.Test protected int Port { get; private set; } + protected PostgresOptions PostgresOptions { get; set; } = new (); + protected override string RootUrl => $"http://localhost:{Port}/"; protected override string ApiKey => _runner.ApiKey; @@ -27,7 +35,14 @@ namespace NzbDrone.Integration.Test { Port = Interlocked.Increment(ref StaticPort); - _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), Port); + PostgresOptions = PostgresDatabase.GetTestOptions(); + + if (PostgresOptions?.Host != null) + { + CreatePostgresDb(PostgresOptions); + } + + _runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), PostgresOptions, Port); _runner.Kill(); _runner.Start(); @@ -56,6 +71,22 @@ namespace NzbDrone.Integration.Test protected override void StopTestTarget() { _runner.Kill(); + if (PostgresOptions?.Host != null) + { + DropPostgresDb(PostgresOptions); + } + } + + private static void CreatePostgresDb(PostgresOptions options) + { + PostgresDatabase.Create(options, MigrationType.Main); + PostgresDatabase.Create(options, MigrationType.Log); + } + + private static void DropPostgresDb(PostgresOptions options) + { + PostgresDatabase.Drop(options, MigrationType.Main); + PostgresDatabase.Drop(options, MigrationType.Log); } } } diff --git a/src/NzbDrone.Mono.Test/DiskProviderTests/SymlinkResolverFixture.cs b/src/NzbDrone.Mono.Test/DiskProviderTests/SymlinkResolverFixture.cs index ac7fadd60..8b4bf2aee 100644 --- a/src/NzbDrone.Mono.Test/DiskProviderTests/SymlinkResolverFixture.cs +++ b/src/NzbDrone.Mono.Test/DiskProviderTests/SymlinkResolverFixture.cs @@ -8,14 +8,9 @@ using NzbDrone.Test.Common; namespace NzbDrone.Mono.Test.DiskProviderTests { [TestFixture] - [Platform("Mono")] + [Platform(Exclude = "Win")] public class SymbolicLinkResolverFixture : TestBase { - public SymbolicLinkResolverFixture() - { - MonoOnly(); - } - [Test] public void should_follow_nested_symlinks() { diff --git a/src/NzbDrone.Test.Common/Datastore/PostgresDatabase.cs b/src/NzbDrone.Test.Common/Datastore/PostgresDatabase.cs new file mode 100644 index 000000000..940334ca2 --- /dev/null +++ b/src/NzbDrone.Test.Common/Datastore/PostgresDatabase.cs @@ -0,0 +1,69 @@ +using System; +using Npgsql; +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Test.Common.Datastore +{ + public static class PostgresDatabase + { + public static PostgresOptions GetTestOptions() + { + var options = PostgresOptions.GetOptions(); + + var uid = TestBase.GetUID(); + options.MainDb = uid + "_main"; + options.LogDb = uid + "_log"; + + return options; + } + + public static void Create(PostgresOptions options, MigrationType migrationType) + { + var db = GetDatabaseName(options, migrationType); + var connectionString = GetConnectionString(options); + using var conn = new NpgsqlConnection(connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"CREATE DATABASE \"{db}\" WITH OWNER = {options.User} ENCODING = 'UTF8' CONNECTION LIMIT = -1;"; + cmd.ExecuteNonQuery(); + } + + public static void Drop(PostgresOptions options, MigrationType migrationType) + { + var db = GetDatabaseName(options, migrationType); + var connectionString = GetConnectionString(options); + using var conn = new NpgsqlConnection(connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = $"DROP DATABASE \"{db}\" WITH (FORCE);"; + cmd.ExecuteNonQuery(); + } + + private static string GetConnectionString(PostgresOptions options) + { + var builder = new NpgsqlConnectionStringBuilder() + { + Host = options.Host, + Port = options.Port, + Username = options.User, + Password = options.Password, + Enlist = false + }; + + return builder.ConnectionString; + } + + private static string GetDatabaseName(PostgresOptions options, MigrationType migrationType) + { + return migrationType switch + { + MigrationType.Main => options.MainDb, + MigrationType.Log => options.LogDb, + _ => throw new NotImplementedException("Unknown migration type") + }; + } + } +} diff --git a/src/NzbDrone.Test.Common/Datastore/SqliteDatabase.cs b/src/NzbDrone.Test.Common/Datastore/SqliteDatabase.cs new file mode 100644 index 000000000..151e245fc --- /dev/null +++ b/src/NzbDrone.Test.Common/Datastore/SqliteDatabase.cs @@ -0,0 +1,14 @@ +using System.IO; +using NUnit.Framework; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Test.Common.Datastore +{ + public static class SqliteDatabase + { + public static string GetCachedDb(MigrationType type) + { + return Path.Combine(TestContext.CurrentContext.TestDirectory, $"cached_{type}.db"); + } + } +} diff --git a/src/NzbDrone.Test.Common/NzbDroneRunner.cs b/src/NzbDrone.Test.Common/NzbDroneRunner.cs index 8ea3dd5ce..b4c97847c 100644 --- a/src/NzbDrone.Test.Common/NzbDroneRunner.cs +++ b/src/NzbDrone.Test.Common/NzbDroneRunner.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Specialized; using System.Diagnostics; using System.IO; using System.Linq; @@ -9,7 +10,9 @@ using NUnit.Framework; using NzbDrone.Common.EnvironmentInfo; using NzbDrone.Common.Extensions; using NzbDrone.Common.Processes; +using NzbDrone.Common.Serializer; using NzbDrone.Core.Configuration; +using NzbDrone.Core.Datastore; using RestSharp; namespace NzbDrone.Test.Common @@ -22,13 +25,15 @@ namespace NzbDrone.Test.Common public string AppData { get; private set; } public string ApiKey { get; private set; } + public PostgresOptions PostgresOptions { get; private set; } public int Port { get; private set; } - public NzbDroneRunner(Logger logger, int port = 9696) + public NzbDroneRunner(Logger logger, PostgresOptions postgresOptions, int port = 9696) { _processProvider = new ProcessProvider(logger); _restClient = new RestClient($"http://localhost:{port}/api/v1"); + PostgresOptions = postgresOptions; Port = port; } @@ -133,9 +138,23 @@ namespace NzbDrone.Test.Common private void Start(string outputProwlarrConsoleExe) { + StringDictionary envVars = new (); + if (PostgresOptions?.Host != null) + { + envVars.Add("Prowlarr__Postgres__Host", PostgresOptions.Host); + envVars.Add("Prowlarr__Postgres__Port", PostgresOptions.Port.ToString()); + envVars.Add("Prowlarr__Postgres__User", PostgresOptions.User); + envVars.Add("Prowlarr__Postgres__Password", PostgresOptions.Password); + envVars.Add("Prowlarr__Postgres__MainDb", PostgresOptions.MainDb); + envVars.Add("Prowlarr__Postgres__LogDb", PostgresOptions.LogDb); + + TestContext.Progress.WriteLine("Using env vars:\n{0}", envVars.ToJson()); + } + TestContext.Progress.WriteLine("Starting instance from {0} on port {1}", outputProwlarrConsoleExe, Port); + var args = "-nobrowser -nosingleinstancecheck -data=\"" + AppData + "\""; - _nzbDroneProcess = _processProvider.Start(outputProwlarrConsoleExe, args, null, OnOutputDataReceived, OnOutputDataReceived); + _nzbDroneProcess = _processProvider.Start(outputProwlarrConsoleExe, args, envVars, OnOutputDataReceived, OnOutputDataReceived); } private void OnOutputDataReceived(string data) diff --git a/src/NzbDrone.Test.Common/TestBase.cs b/src/NzbDrone.Test.Common/TestBase.cs index fa25c039f..4ca32b7c7 100644 --- a/src/NzbDrone.Test.Common/TestBase.cs +++ b/src/NzbDrone.Test.Common/TestBase.cs @@ -166,14 +166,6 @@ namespace NzbDrone.Test.Common } } - protected void MonoOnly() - { - if (!PlatformInfo.IsMono) - { - throw new IgnoreException("mono specific test"); - } - } - protected void NotBsd() { if (OsInfo.Os == Os.Bsd) diff --git a/src/postgres.runsettings b/src/postgres.runsettings new file mode 100644 index 000000000..fa0a59c96 --- /dev/null +++ b/src/postgres.runsettings @@ -0,0 +1,11 @@ + + + + + 192.168.100.5 + 5432 + abc + abc + + +