diff --git a/src/Tests/AliasVault.IntegrationTests/AbstractTestHostBuilder.cs b/src/Tests/AliasVault.IntegrationTests/AbstractTestHostBuilder.cs new file mode 100644 index 000000000..7d88f0909 --- /dev/null +++ b/src/Tests/AliasVault.IntegrationTests/AbstractTestHostBuilder.cs @@ -0,0 +1,161 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +// ----------------------------------------------------------------------- + +namespace AliasVault.IntegrationTests; + +using AliasServerDb; +using AliasServerDb.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Npgsql; + +/// +/// Builder class for creating a test host for services in order to run integration tests against them. This class +/// contains common logic such as creating a temporary database. +/// +public class AbstractTestHostBuilder : IAsyncDisposable +{ + /// + /// The DbContextFactory instance that is created for the test. + /// + private IAliasServerDbContextFactory _dbContextFactory = null!; + + /// + /// The cached DbContext instance that can be used during the test. + /// + private AliasServerDbContext? _dbContext; + + /// + /// The temporary database name for the test. + /// + private string? _tempDbName; + + /// + /// Returns the DbContext instance for the test. This can be used to seed the database with test data. + /// + /// AliasServerDbContext instance. + public AliasServerDbContext GetDbContext() + { + if (_dbContext != null) + { + return _dbContext; + } + + _dbContext = _dbContextFactory.CreateDbContext(); + return _dbContext; + } + + /// + /// Returns the DbContext instance for the test. This can be used to seed the database with test data. + /// + /// AliasServerDbContext instance. + public async Task GetDbContextAsync() + { + return await _dbContextFactory.CreateDbContextAsync(); + } + + /// + /// Disposes of the test host and cleans up the temporary database. + /// + /// A representing the asynchronous operation. + public async ValueTask DisposeAsync() + { + if (_dbContext != null) + { + await _dbContext.DisposeAsync(); + _dbContext = null; + } + + if (!string.IsNullOrEmpty(_tempDbName)) + { + // Create a connection to 'postgres' database to drop the test database + using var conn = + new NpgsqlConnection( + "Host=localhost;Port=5433;Database=postgres;Username=aliasvault;Password=password"); + await conn.OpenAsync(); + + // First terminate existing connections + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = $""" + SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = '{_tempDbName}'; + """; + await cmd.ExecuteNonQueryAsync(); + } + + // Then drop the database + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = $""" + DROP DATABASE IF EXISTS "{_tempDbName}"; + """; + await cmd.ExecuteNonQueryAsync(); + } + + GC.SuppressFinalize(this); + } + } + + /// + /// Creates a new test host builder with test database connection already configured. + /// + /// IHost. + protected IHostBuilder CreateBuilder() + { + _tempDbName = $"aliasdb_test_{Guid.NewGuid()}"; + + // Create a connection to 'postgres' database to ensure the test database exists + AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); + + using (var conn = new NpgsqlConnection("Host=localhost;Port=5433;Database=postgres;Username=aliasvault;Password=password")) + { + conn.Open(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = $""" + CREATE DATABASE "{_tempDbName}"; + """; + cmd.ExecuteNonQuery(); + } + } + + // Create a connection to the new test database + var dbConnection = new NpgsqlConnection($"Host=localhost;Port=5433;Database={_tempDbName};Username=aliasvault;Password=password"); + + var builder = Host.CreateDefaultBuilder() + .ConfigureServices((context, services) => + { + // Override configuration + var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true) + .AddInMemoryCollection(new Dictionary + { + ["DatabaseProvider"] = "postgresql", + ["ConnectionStrings:AliasServerDbContext"] = dbConnection.ConnectionString, + }) + .Build(); + + services.AddSingleton(configuration); + services.AddAliasVaultDatabaseConfiguration(configuration); + + // Ensure the in-memory database is populated with tables + var serviceProvider = services.BuildServiceProvider(); + using (var scope = serviceProvider.CreateScope()) + { + _dbContextFactory = scope.ServiceProvider.GetRequiredService(); + var dbContext = _dbContextFactory.CreateDbContext(); + dbContext.Database.Migrate(); + } + }); + + return builder; + } +} diff --git a/src/Tests/AliasVault.IntegrationTests/SmtpServer/TestHostBuilder.cs b/src/Tests/AliasVault.IntegrationTests/SmtpServer/TestHostBuilder.cs index 519482e79..e53a0bb65 100644 --- a/src/Tests/AliasVault.IntegrationTests/SmtpServer/TestHostBuilder.cs +++ b/src/Tests/AliasVault.IntegrationTests/SmtpServer/TestHostBuilder.cs @@ -7,185 +7,60 @@ namespace AliasVault.IntegrationTests.SmtpServer; -using System.Data.Common; -using AliasServerDb; -using AliasServerDb.Configuration; using AliasVault.SmtpService; using AliasVault.SmtpService.Handlers; using AliasVault.SmtpService.Workers; using global::SmtpServer; using global::SmtpServer.Storage; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Npgsql; /// /// Builder class for creating a test host for the SmtpServiceWorker in order to run integration tests against it. /// -public class TestHostBuilder : IAsyncDisposable +public class TestHostBuilder : AbstractTestHostBuilder { /// - /// The DbContextFactory instance that is created for the test. - /// - private IAliasServerDbContextFactory _dbContextFactory = null!; - - /// - /// The cached DbContext instance that can be used during the test. - /// - private AliasServerDbContext? _dbContext; - - /// - /// The temporary database name for the test. - /// - private string? _tempDbName; - - /// - /// Returns the DbContext instance for the test. This can be used to seed the database with test data. - /// - /// AliasServerDbContext instance. - public AliasServerDbContext GetDbContext() - { - if (_dbContext != null) - { - return _dbContext; - } - - _dbContext = _dbContextFactory.CreateDbContext(); - return _dbContext; - } - - /// - /// Builds the SmtpService test host. + /// Builds the SmtpService test host with a provided database connection. /// /// IHost. public IHost Build() { - _tempDbName = $"aliasdb_test_{Guid.NewGuid()}"; + // Get base builder with database connection already configured. + var builder = CreateBuilder(); - // Create a connection to 'postgres' database to ensure the test database exists - using (var conn = new NpgsqlConnection("Host=localhost;Port=5433;Database=postgres;Username=aliasvault;Password=password")) + // Add specific services for the TestExceptionWorker. + builder.ConfigureServices((context, services) => { - conn.Open(); - using (var cmd = conn.CreateCommand()) + services.AddSingleton(new Config { - cmd.CommandText = $""" - CREATE DATABASE "{_tempDbName}"; - """; - cmd.ExecuteNonQuery(); - } - } + AllowedToDomains = new List { "example.tld" }, + SmtpTlsEnabled = "false", + }); - // Create a connection to the new test database - var dbConnection = new NpgsqlConnection($"Host=localhost;Port=5433;Database={_tempDbName};Username=aliasvault;Password=password"); - - return Build(dbConnection); - } - - /// - /// Builds the SmtpService test host with a provided database connection. - /// - /// The database connection to use for the test. - /// IHost. - public IHost Build(DbConnection dbConnection) - { - var builder = Host.CreateDefaultBuilder() - .ConfigureServices((context, services) => - { - // Override configuration - var configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json", optional: true) - .AddInMemoryCollection(new Dictionary - { - ["DatabaseProvider"] = "postgresql", - ["ConnectionStrings:AliasServerDbContext"] = dbConnection.ConnectionString, - }) - .Build(); - - services.AddSingleton(configuration); - - services.AddSingleton(new Config + services.AddTransient(); + services.AddSingleton( + provider => { - AllowedToDomains = new List { "example.tld" }, - SmtpTlsEnabled = "false", + var options = new SmtpServerOptionsBuilder() + .ServerName("aliasvault"); + + // Note: port 25 doesn't work in GitHub actions so we use these instead for the integration tests: + // - 2525 for the SMTP server + // - 5870 for the submission server + options.Endpoint(serverBuilder => + serverBuilder + .Port(2525, false)) + .Endpoint(serverBuilder => + serverBuilder + .Port(5870, false)); + + return new SmtpServer(options.Build(), provider.GetRequiredService()); }); - services.AddTransient(); - services.AddSingleton( - provider => - { - var options = new SmtpServerOptionsBuilder() - .ServerName("aliasvault"); - - // Note: port 25 doesn't work in GitHub actions so we use these instead for the integration tests: - // - 2525 for the SMTP server - // - 5870 for the submission server - options.Endpoint(serverBuilder => - serverBuilder - .Port(2525, false)) - .Endpoint(serverBuilder => - serverBuilder - .Port(5870, false)); - - return new SmtpServer(options.Build(), provider.GetRequiredService()); - }); - - services.AddAliasVaultDatabaseConfiguration(configuration); - services.AddHostedService(); - - // Ensure the in-memory database is populated with tables - var serviceProvider = services.BuildServiceProvider(); - using (var scope = serviceProvider.CreateScope()) - { - _dbContextFactory = scope.ServiceProvider.GetRequiredService(); - var dbContext = _dbContextFactory.CreateDbContext(); - dbContext.Database.Migrate(); - } - }); + services.AddHostedService(); + }); return builder.Build(); } - - /// - /// Disposes of the test host and cleans up the temporary database. - /// - /// A representing the asynchronous operation. - public async ValueTask DisposeAsync() - { - if (_dbContext != null) - { - await _dbContext.DisposeAsync(); - _dbContext = null; - } - - if (!string.IsNullOrEmpty(_tempDbName)) - { - // Create a connection to 'postgres' database to drop the test database - using var conn = new NpgsqlConnection("Host=localhost;Port=5433;Database=postgres;Username=aliasvault;Password=password"); - await conn.OpenAsync(); - - // First terminate existing connections - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = $""" - SELECT pg_terminate_backend(pid) - FROM pg_stat_activity - WHERE datname = '{_tempDbName}'; - """; - await cmd.ExecuteNonQueryAsync(); - } - - // Then drop the database - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = $""" - DROP DATABASE IF EXISTS "{_tempDbName}"; - """; - await cmd.ExecuteNonQueryAsync(); - } - - GC.SuppressFinalize(this); - } - } } diff --git a/src/Tests/AliasVault.IntegrationTests/StatusHostedService/StatusHostedServiceTests.cs b/src/Tests/AliasVault.IntegrationTests/StatusHostedService/StatusHostedServiceTests.cs new file mode 100644 index 000000000..c03fd43f4 --- /dev/null +++ b/src/Tests/AliasVault.IntegrationTests/StatusHostedService/StatusHostedServiceTests.cs @@ -0,0 +1,73 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.IntegrationTests.StatusHostedService; + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Hosting; + +/// +/// Integration tests for StatusHostedService wrapper. +/// +[TestFixture] +public class StatusHostedServiceTests +{ + /// + /// The test host instance. + /// + private IHost _testHost; + + /// + /// The test host builder instance. + /// + private TestHostBuilder _testHostBuilder; + + /// + /// Setup logic for every test. + /// + [SetUp] + public void Setup() + { + _testHostBuilder = new TestHostBuilder(); + _testHost = _testHostBuilder.Build(); + } + + /// + /// Tear down logic for every test. + /// + /// Task. + [TearDown] + public async Task TearDown() + { + await _testHost.StopAsync(); + _testHost.Dispose(); + await _testHostBuilder.DisposeAsync(); + } + + /// + /// Tests that the StatusHostedService properly logs errors from the wrapped service. + /// + /// Task. + [Test] + public async Task LogsExceptionFromWrappedService() + { + // Start the service which will trigger the TestExceptionWorker to throw + await _testHost.StartAsync(); + + // Give it a moment to process + await Task.Delay(5000); + + // Check the logs for the expected error + await using var dbContext = _testHostBuilder.GetDbContext(); + var errorLog = await dbContext.Logs + .OrderByDescending(l => l.TimeStamp) + .FirstOrDefaultAsync(l => l.Level == "Error" && l.Exception.Contains("Test exception")); + + Assert.That(errorLog, Is.Not.Null, "Expected error log from TestExceptionWorker was not found"); + Assert.That(errorLog.Message, Does.Contain("An error occurred in StatusHostedService"), "Error log does not contain expected message from StatusHostedService"); + } +} diff --git a/src/Tests/AliasVault.IntegrationTests/StatusHostedService/TestExceptionWorker.cs b/src/Tests/AliasVault.IntegrationTests/StatusHostedService/TestExceptionWorker.cs new file mode 100644 index 000000000..d4f93dff8 --- /dev/null +++ b/src/Tests/AliasVault.IntegrationTests/StatusHostedService/TestExceptionWorker.cs @@ -0,0 +1,23 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.IntegrationTests.StatusHostedService; + +using Microsoft.Extensions.Hosting; + +/// +/// A simple worker that throws an exception during task execution. This is used for testing purposes. +/// +public class TestExceptionWorker() : BackgroundService +{ + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await Task.Delay(TimeSpan.FromMilliseconds(100), stoppingToken); + throw new Exception("Test exception"); + } +} diff --git a/src/Tests/AliasVault.IntegrationTests/StatusHostedService/TestHostBuilder.cs b/src/Tests/AliasVault.IntegrationTests/StatusHostedService/TestHostBuilder.cs new file mode 100644 index 000000000..b0200b0df --- /dev/null +++ b/src/Tests/AliasVault.IntegrationTests/StatusHostedService/TestHostBuilder.cs @@ -0,0 +1,49 @@ +// ----------------------------------------------------------------------- +// +// Copyright (c) lanedirt. All rights reserved. +// Licensed under the MIT license. See LICENSE.md file in the project root for full license information. +// +// ----------------------------------------------------------------------- + +namespace AliasVault.IntegrationTests.StatusHostedService; + +using System.Data.Common; +using System.Reflection; +using AliasServerDb; +using AliasServerDb.Configuration; +using AliasVault.WorkerStatus.ServiceExtensions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Npgsql; + +/// +/// Builder class for creating a test host for the StatusHostedService wrapper in order to run integration tests +/// against it. This primarily tests basic functionality of the hosted service such as starting, stopping and error +/// handling. +/// +/// The StatusHostedService is a wrapper around the HostedService class that provides additional functionality for +/// managing the status of the hosted service. This includes being able to start and stop the services from the +/// AliasVault admin panel. +/// +public class TestHostBuilder : AbstractTestHostBuilder +{ + /// + /// Builds the test host for the TestExceptionWorker. + /// + /// IHost. + public IHost Build() + { + // Get base builder with database connection already configured. + var builder = CreateBuilder(); + + // Add specific services for the TestExceptionWorker. + builder.ConfigureServices((context, services) => + { + services.AddStatusHostedService(Assembly.GetExecutingAssembly().GetName().Name!); + }); + + return builder.Build(); + } +} diff --git a/src/Tests/AliasVault.IntegrationTests/TaskRunner/TestHostBuilder.cs b/src/Tests/AliasVault.IntegrationTests/TaskRunner/TestHostBuilder.cs index 71e109494..da4dfb135 100644 --- a/src/Tests/AliasVault.IntegrationTests/TaskRunner/TestHostBuilder.cs +++ b/src/Tests/AliasVault.IntegrationTests/TaskRunner/TestHostBuilder.cs @@ -7,149 +7,41 @@ namespace AliasVault.IntegrationTests.TaskRunner; -using AliasServerDb; -using AliasServerDb.Configuration; using AliasVault.Shared.Server.Services; using AliasVault.TaskRunner.Tasks; using AliasVault.TaskRunner.Workers; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Npgsql; /// /// Builder class for creating a test host for the TaskRunner in order to run integration tests against it. /// -public class TestHostBuilder : IAsyncDisposable +public class TestHostBuilder : AbstractTestHostBuilder { - /// - /// The DbContextFactory instance that is created for the test. - /// - private IAliasServerDbContextFactory _dbContextFactory = null!; - - /// - /// The cached DbContext instance that can be used during the test. - /// - private AliasServerDbContext? _dbContext; - - /// - /// The temporary database name for the test. - /// - private string? _tempDbName; - - /// - /// Returns the DbContext instance for the test. This can be used to seed the database with test data. - /// - /// AliasServerDbContext instance. - public async Task GetDbContextAsync() - { - return await _dbContextFactory.CreateDbContextAsync(); - } - /// /// Builds the TaskRunner test host. /// /// IHost. public IHost Build() { - // Create a temporary database for the test - _tempDbName = $"aliasdb_test_{Guid.NewGuid()}"; + // Get base builder with database connection already configured. + var builder = CreateBuilder(); - // Create a connection to 'postgres' database to create the test database - using (var conn = new NpgsqlConnection("Host=localhost;Port=5433;Database=postgres;Username=aliasvault;Password=password")) + // Add specific services for the TestExceptionWorker. + builder.ConfigureServices((context, services) => { - conn.Open(); - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = $""" - CREATE DATABASE "{_tempDbName}"; - """; - cmd.ExecuteNonQuery(); - } - } + // Add server settings service + services.AddSingleton(); - // Create the connection to the new test database - var dbConnection = new NpgsqlConnection($"Host=localhost;Port=5433;Database={_tempDbName};Username=aliasvault;Password=password"); - var builder = Host.CreateDefaultBuilder() - .ConfigureServices((context, services) => - { - // Override configuration - var configuration = new ConfigurationBuilder() - .AddJsonFile("appsettings.json", optional: true) - .AddInMemoryCollection(new Dictionary - { - ["DatabaseProvider"] = "postgresql", - ["ConnectionStrings:AliasServerDbContext"] = dbConnection.ConnectionString, - }) - .Build(); - services.AddSingleton(configuration); + // Add maintenance tasks + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); - // Add server settings service - services.AddSingleton(); - - // Add maintenance tasks - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - services.AddAliasVaultDatabaseConfiguration(configuration); - - // Add the TaskRunner worker - services.AddHostedService(); - - // Ensure the database is populated with tables - var serviceProvider = services.BuildServiceProvider(); - using (var scope = serviceProvider.CreateScope()) - { - _dbContextFactory = scope.ServiceProvider.GetRequiredService(); - var dbContext = _dbContextFactory.CreateDbContext(); - dbContext.Database.Migrate(); - } - }); + // Add the TaskRunner worker + services.AddHostedService(); + }); return builder.Build(); } - - /// - /// Disposes of the test host and cleans up the temporary database. - /// - /// A representing the asynchronous operation. - public async ValueTask DisposeAsync() - { - if (_dbContext != null) - { - await _dbContext.DisposeAsync(); - _dbContext = null; - } - - if (!string.IsNullOrEmpty(_tempDbName)) - { - // Create a connection to 'postgres' database to drop the test database - using var conn = new NpgsqlConnection("Host=localhost;Port=5433;Database=postgres;Username=aliasvault;Password=password"); - await conn.OpenAsync(); - - // First terminate existing connections - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = $""" - SELECT pg_terminate_backend(pid) - FROM pg_stat_activity - WHERE datname = '{_tempDbName}'; - """; - await cmd.ExecuteNonQueryAsync(); - } - - // Then drop the database - using (var cmd = conn.CreateCommand()) - { - cmd.CommandText = $""" - DROP DATABASE IF EXISTS "{_tempDbName}"; - """; - await cmd.ExecuteNonQueryAsync(); - } - } - - GC.SuppressFinalize(this); - } }