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);
- }
}