From 24cd1e6da96dcea8bc40889afd8f80073eb73147 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:09:33 -0400 Subject: [PATCH 01/16] Use NpgsqlDataSourceBuilder instead of global type mapper. --- LeaderboardBackend/Models/Entities/ApplicationContext.cs | 8 -------- LeaderboardBackend/Program.cs | 4 +++- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/LeaderboardBackend/Models/Entities/ApplicationContext.cs b/LeaderboardBackend/Models/Entities/ApplicationContext.cs index 29cea966..7c94e8d9 100644 --- a/LeaderboardBackend/Models/Entities/ApplicationContext.cs +++ b/LeaderboardBackend/Models/Entities/ApplicationContext.cs @@ -7,14 +7,6 @@ namespace LeaderboardBackend.Models.Entities; public class ApplicationContext : DbContext { public const string CASE_INSENSITIVE_COLLATION = "case_insensitive"; - - [Obsolete] - static ApplicationContext() - { - // GlobalTypeMapper is obsolete but the new way (DataSource) is a pain to work with - NpgsqlConnection.GlobalTypeMapper.MapEnum(); - } - public ApplicationContext(DbContextOptions options) : base(options) { } diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index dfc5199a..0b6bb690 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -84,7 +84,9 @@ connectionBuilder.Port = db.Port.Value; } - opt.UseNpgsql(connectionBuilder.ConnectionString, o => o.UseNodaTime()); + NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); + dataSourceBuilder.UseNodaTime().MapEnum(); + opt.UseNpgsql(dataSourceBuilder.Build(), o => o.UseNodaTime()); opt.UseSnakeCaseNamingConvention(); } else From e12dac829948d68bf9c552274b3dcd6c054d95a1 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:10:15 -0400 Subject: [PATCH 02/16] Migrate the database asynchronously. --- .../Models/Entities/ApplicationContext.cs | 18 +++++++++++++----- LeaderboardBackend/Program.cs | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/LeaderboardBackend/Models/Entities/ApplicationContext.cs b/LeaderboardBackend/Models/Entities/ApplicationContext.cs index 7c94e8d9..2d3e59f7 100644 --- a/LeaderboardBackend/Models/Entities/ApplicationContext.cs +++ b/LeaderboardBackend/Models/Entities/ApplicationContext.cs @@ -20,14 +20,22 @@ public ApplicationContext(DbContextOptions options) /// /// Migrates the database and reloads Npgsql types /// - public void MigrateDatabase() + public async Task MigrateDatabase() { - Database.Migrate(); + await Database.MigrateAsync(); // when new extensions have been enabled by migrations, Npgsql's type cache must be refreshed - Database.OpenConnection(); - ((NpgsqlConnection)Database.GetDbConnection()).ReloadTypes(); - Database.CloseConnection(); + NpgsqlConnection connection = (NpgsqlConnection)Database.GetDbConnection(); + await connection.OpenAsync(); + + try + { + await connection.ReloadTypesAsync(); + } + finally + { + await connection.CloseAsync(); + } } protected override void OnModelCreating(ModelBuilder modelBuilder) diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index 0b6bb690..14649f2f 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -313,7 +313,7 @@ if (config.MigrateDb && app.Environment.IsDevelopment()) { // migration as part of the startup phase (dev env only) - context.MigrateDatabase(); + await context.MigrateDatabase(); } } From d594367f22afe61d8df3d14e7776327152ad2d3d Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:21:06 -0400 Subject: [PATCH 03/16] Set up the test DB. --- .../LeaderboardBackend.Test.csproj | 1 + .../TestApi/TestApiFactory.cs | 86 +++++++++++++++---- .../Models/Entities/ApplicationContext.cs | 18 +++- LeaderboardBackend/Program.cs | 2 +- 4 files changed, 89 insertions(+), 18 deletions(-) diff --git a/LeaderboardBackend.Test/LeaderboardBackend.Test.csproj b/LeaderboardBackend.Test/LeaderboardBackend.Test.csproj index 3cc526c2..eaab3c40 100644 --- a/LeaderboardBackend.Test/LeaderboardBackend.Test.csproj +++ b/LeaderboardBackend.Test/LeaderboardBackend.Test.csproj @@ -25,6 +25,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs index 832ac50c..90825ec6 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs @@ -1,27 +1,34 @@ using System; +using System.Linq; using System.Net.Http; +using System.Threading.Tasks; using LeaderboardBackend.Models.Entities; using LeaderboardBackend.Test.Lib; using MailKit.Net.Smtp; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Moq; using BCryptNet = BCrypt.Net.BCrypt; +using Npgsql; +using Respawn; +using Respawn.Graph; namespace LeaderboardBackend.Test.TestApi; public class TestApiFactory : WebApplicationFactory { + private static bool _migrated = false; + protected override void ConfigureWebHost(IWebHostBuilder builder) { // Set the environment for the run to Staging builder.UseEnvironment(Environments.Staging); base.ConfigureWebHost(builder); - builder.ConfigureServices(services => { if (PostgresDatabaseFixture.PostgresContainer is null) @@ -29,16 +36,48 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) throw new InvalidOperationException("Postgres container is not initialized."); } - services.Configure(conf => + ServiceDescriptor? dbContextDescriptor = services.SingleOrDefault( + d => d.ServiceType == + typeof(DbContextOptions)); + + if (dbContextDescriptor is not null) + { + services.Remove(dbContextDescriptor); + } + + PostgresConfig db = new() + { + Db = PostgresDatabaseFixture.Database!, + Port = (ushort)PostgresDatabaseFixture.Port, + Host = PostgresDatabaseFixture.PostgresContainer.Hostname, + User = PostgresDatabaseFixture.Username!, + Password = PostgresDatabaseFixture.Password!, + }; + + NpgsqlConnectionStringBuilder connectionBuilder = new() + { + Host = db.Host, + Username = db.User, + Password = db.Password, + Database = db.Db, + IncludeErrorDetail = true, + }; + + if (db.Port is not null) + { + connectionBuilder.Port = db.Port.Value; + } + + services.AddSingleton(container => { - conf.Pg = new PostgresConfig - { - Db = PostgresDatabaseFixture.Database!, - Port = (ushort)PostgresDatabaseFixture.Port, - Host = PostgresDatabaseFixture.PostgresContainer.Hostname, - User = PostgresDatabaseFixture.Username!, - Password = PostgresDatabaseFixture.Password! - }; + NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); + dataSourceBuilder.UseNodaTime().MapEnum(); + return dataSourceBuilder.Build(); + }); + + services.AddDbContext((container, options) => { + NpgsqlDataSource dataSource = container.GetRequiredService(); + options.UseNpgsql(dataSource, o => o.UseNodaTime()).UseSnakeCaseNamingConvention(); }); // mock SMTP client @@ -66,14 +105,13 @@ public void InitializeDatabase() private static void InitializeDatabase(ApplicationContext dbContext) { - if (!PostgresDatabaseFixture.HasCreatedTemplate) + if (!_migrated) { dbContext.MigrateDatabase(); + _migrated = true; Seed(dbContext); - PostgresDatabaseFixture.CreateTemplateFromCurrentDb(); } } - private static void Seed(ApplicationContext dbContext) { Leaderboard leaderboard = @@ -94,12 +132,28 @@ private static void Seed(ApplicationContext dbContext) dbContext.SaveChanges(); } - /// /// Deletes and recreates the database /// - public void ResetDatabase() + public async Task ResetDatabase() { - PostgresDatabaseFixture.ResetDatabaseToTemplate(); + using NpgsqlConnection conn = new(PostgresDatabaseFixture.PostgresContainer!.GetConnectionString()); + await conn.OpenAsync(); + + Respawner respawner = await Respawner.CreateAsync(conn, new RespawnerOptions + { + TablesToInclude = new Table[] + { + "users", + "categories", + "leaderboards", + "account_confirmations", + "account_recoveries", + "runs" + }, + DbAdapter = DbAdapter.Postgres + }); + + await respawner.ResetAsync(conn); } } diff --git a/LeaderboardBackend/Models/Entities/ApplicationContext.cs b/LeaderboardBackend/Models/Entities/ApplicationContext.cs index 2d3e59f7..61d7b627 100644 --- a/LeaderboardBackend/Models/Entities/ApplicationContext.cs +++ b/LeaderboardBackend/Models/Entities/ApplicationContext.cs @@ -17,10 +17,26 @@ public ApplicationContext(DbContextOptions options) public DbSet Runs { get; set; } = null!; public DbSet Users { get; set; } = null!; + public void MigrateDatabase() + { + Database.Migrate(); + NpgsqlConnection connection = (NpgsqlConnection)Database.GetDbConnection(); + connection.Open(); + + try + { + connection.ReloadTypes(); + } + finally + { + connection.Close(); + } + } + /// /// Migrates the database and reloads Npgsql types /// - public async Task MigrateDatabase() + public async Task MigrateDatabaseAsync() { await Database.MigrateAsync(); diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index 14649f2f..d29b8e59 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -313,7 +313,7 @@ if (config.MigrateDb && app.Environment.IsDevelopment()) { // migration as part of the startup phase (dev env only) - await context.MigrateDatabase(); + await context.MigrateDatabaseAsync(); } } From 1d48c943df04d4fe60aac77d3b40a67f2b6a0261 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:22:25 -0400 Subject: [PATCH 04/16] Don't use a template database. --- .../Fixtures/PostgresDatabaseFixture.cs | 62 +------------------ 1 file changed, 1 insertion(+), 61 deletions(-) diff --git a/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs b/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs index 96e3ebf6..0f0146f9 100644 --- a/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs +++ b/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using LeaderboardBackend.Test.Fixtures; using Npgsql; using NUnit.Framework; using Testcontainers.PostgreSql; @@ -9,15 +8,13 @@ // It has no namespace on purpose, so that the fixture applies to all tests in this assembly [SetUpFixture] // https://docs.nunit.org/articles/nunit/writing-tests/attributes/setupfixture.html -internal class PostgresDatabaseFixture +public class PostgresDatabaseFixture { public static PostgreSqlContainer? PostgresContainer { get; private set; } public static string? Database { get; private set; } public static string? Username { get; private set; } public static string? Password { get; private set; } public static int Port { get; private set; } - public static bool HasCreatedTemplate { get; private set; } = false; - private static string TemplateDatabase => Database! + "_template"; [OneTimeSetUp] public static async Task OneTimeSetup() @@ -44,61 +41,4 @@ public static async Task OneTimeTearDown() await PostgresContainer.DisposeAsync(); } - - public static void CreateTemplateFromCurrentDb() - { - ThrowIfNotInitialized(); - - NpgsqlConnection.ClearAllPools(); // can't drop a DB if connections remain open - using NpgsqlDataSource conn = CreateConnectionToTemplate(); - conn.CreateCommand( - @$" - DROP DATABASE IF EXISTS {TemplateDatabase}; - CREATE DATABASE {TemplateDatabase} - WITH TEMPLATE {Database} - OWNER '{Username}'; - " - ) - .ExecuteNonQuery(); - HasCreatedTemplate = true; - } - - // It is faster to recreate the db from an already seeded template - // compared to dropping the db and recreating it from scratch - public static void ResetDatabaseToTemplate() - { - ThrowIfNotInitialized(); - if (!HasCreatedTemplate) - { - throw new InvalidOperationException("Database template has not been created."); - } - - NpgsqlConnection.ClearAllPools(); // can't drop a DB if connections remain open - using NpgsqlDataSource conn = CreateConnectionToTemplate(); - conn.CreateCommand( - @$" - DROP DATABASE IF EXISTS {Database}; - CREATE DATABASE {Database} - WITH TEMPLATE {TemplateDatabase} - OWNER '{Username}'; - " - ) - .ExecuteNonQuery(); - } - - private static NpgsqlDataSource CreateConnectionToTemplate() - { - ThrowIfNotInitialized(); - NpgsqlConnectionStringBuilder connStrBuilder = - new(PostgresContainer!.GetConnectionString()) { Database = "template1" }; - return NpgsqlDataSource.Create(connStrBuilder); - } - - private static void ThrowIfNotInitialized() - { - if (PostgresContainer is null) - { - throw new InvalidOperationException("Postgres container is not initialized."); - } - } } From 3294be9590cfe1139f8dad4b20c693dd2db4e735 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:26:22 -0400 Subject: [PATCH 05/16] formatting --- LeaderboardBackend.Test/TestApi/TestApiFactory.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs index 90825ec6..2e70f80d 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs @@ -12,10 +12,10 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Moq; -using BCryptNet = BCrypt.Net.BCrypt; using Npgsql; using Respawn; using Respawn.Graph; +using BCryptNet = BCrypt.Net.BCrypt; namespace LeaderboardBackend.Test.TestApi; @@ -29,6 +29,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.UseEnvironment(Environments.Staging); base.ConfigureWebHost(builder); + builder.ConfigureServices(services => { if (PostgresDatabaseFixture.PostgresContainer is null) @@ -75,7 +76,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) return dataSourceBuilder.Build(); }); - services.AddDbContext((container, options) => { + services.AddDbContext((container, options) => + { NpgsqlDataSource dataSource = container.GetRequiredService(); options.UseNpgsql(dataSource, o => o.UseNodaTime()).UseSnakeCaseNamingConvention(); }); From b0e659caa44af9648f0ab48da6b43d522b5848d5 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:30:39 -0400 Subject: [PATCH 06/16] Fix DB init. --- LeaderboardBackend.Test/TestApi/TestApiFactory.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs index 2e70f80d..b83bd0bd 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs @@ -111,8 +111,9 @@ private static void InitializeDatabase(ApplicationContext dbContext) { dbContext.MigrateDatabase(); _migrated = true; - Seed(dbContext); } + + Seed(dbContext); } private static void Seed(ApplicationContext dbContext) { @@ -157,5 +158,6 @@ public async Task ResetDatabase() }); await respawner.ResetAsync(conn); + InitializeDatabase(); } } From e91e1074326533f87f05ea4a0fbacc9f0824af35 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:37:30 -0400 Subject: [PATCH 07/16] Configure ApplicationContextConfig. --- .../TestApi/TestApiFactory.cs | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs index b83bd0bd..529a2bbb 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -11,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using Moq; using Npgsql; using Respawn; @@ -46,31 +48,43 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) services.Remove(dbContextDescriptor); } - PostgresConfig db = new() - { - Db = PostgresDatabaseFixture.Database!, - Port = (ushort)PostgresDatabaseFixture.Port, - Host = PostgresDatabaseFixture.PostgresContainer.Hostname, - User = PostgresDatabaseFixture.Username!, - Password = PostgresDatabaseFixture.Password!, - }; - - NpgsqlConnectionStringBuilder connectionBuilder = new() - { - Host = db.Host, - Username = db.User, - Password = db.Password, - Database = db.Db, - IncludeErrorDetail = true, - }; - - if (db.Port is not null) - { - connectionBuilder.Port = db.Port.Value; - } + services.Configure(conf => + conf.Pg = new PostgresConfig + { + Db = PostgresDatabaseFixture.Database!, + Port = (ushort)PostgresDatabaseFixture.Port, + Host = PostgresDatabaseFixture.PostgresContainer.Hostname, + User = PostgresDatabaseFixture.Username!, + Password = PostgresDatabaseFixture.Password! + }); services.AddSingleton(container => { + ApplicationContextConfig appConfig = container + .GetRequiredService>() + .Value; + + if (appConfig.Pg is null) + { + throw new UnreachableException( + "The database configuration is invalid but it was not caught by validation!" + ); + } + + NpgsqlConnectionStringBuilder connectionBuilder = new() + { + Host = appConfig.Pg.Host, + Username = appConfig.Pg.User, + Password = appConfig.Pg.Password, + Database = appConfig.Pg.Db, + IncludeErrorDetail = true, + }; + + if (appConfig.Pg.Port is not null) + { + connectionBuilder.Port = appConfig.Pg.Port.Value; + } + NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); dataSourceBuilder.UseNodaTime().MapEnum(); return dataSourceBuilder.Build(); From 590d5feb4cbf66b6c578528dc4fc3c2157fe6acf Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:13:22 -0400 Subject: [PATCH 08/16] Cache the data source. --- .../Fixtures/PostgresDatabaseFixture.cs | 5 ++ .../TestApi/TestApiFactory.cs | 77 ++++++------------- 2 files changed, 29 insertions(+), 53 deletions(-) diff --git a/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs b/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs index 0f0146f9..2c628520 100644 --- a/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs +++ b/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using LeaderboardBackend.Models.Entities; using Npgsql; using NUnit.Framework; using Testcontainers.PostgreSql; @@ -14,6 +15,7 @@ public class PostgresDatabaseFixture public static string? Database { get; private set; } public static string? Username { get; private set; } public static string? Password { get; private set; } + public static NpgsqlDataSource? DataSource { get; private set; } public static int Port { get; private set; } [OneTimeSetUp] @@ -29,6 +31,9 @@ public static async Task OneTimeSetup() Password = connStrBuilder.Password!; Database = connStrBuilder.Database!; Port = connStrBuilder.Port; + NpgsqlDataSourceBuilder dataSourceBuilder = new(connStrBuilder.ConnectionString); + dataSourceBuilder.UseNodaTime().MapEnum(); + DataSource = dataSourceBuilder.Build(); } [OneTimeTearDown] diff --git a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs index 529a2bbb..f5aaccd1 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -12,7 +11,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; using Moq; using Npgsql; using Respawn; @@ -24,7 +22,8 @@ namespace LeaderboardBackend.Test.TestApi; public class TestApiFactory : WebApplicationFactory { private static bool _migrated = false; - + private static bool _seeded = false; + private readonly Mock _mock = new(); protected override void ConfigureWebHost(IWebHostBuilder builder) { // Set the environment for the run to Staging @@ -58,46 +57,13 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) Password = PostgresDatabaseFixture.Password! }); - services.AddSingleton(container => + services.AddDbContext(options => { - ApplicationContextConfig appConfig = container - .GetRequiredService>() - .Value; - - if (appConfig.Pg is null) - { - throw new UnreachableException( - "The database configuration is invalid but it was not caught by validation!" - ); - } - - NpgsqlConnectionStringBuilder connectionBuilder = new() - { - Host = appConfig.Pg.Host, - Username = appConfig.Pg.User, - Password = appConfig.Pg.Password, - Database = appConfig.Pg.Db, - IncludeErrorDetail = true, - }; - - if (appConfig.Pg.Port is not null) - { - connectionBuilder.Port = appConfig.Pg.Port.Value; - } - - NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); - dataSourceBuilder.UseNodaTime().MapEnum(); - return dataSourceBuilder.Build(); - }); - - services.AddDbContext((container, options) => - { - NpgsqlDataSource dataSource = container.GetRequiredService(); - options.UseNpgsql(dataSource, o => o.UseNodaTime()).UseSnakeCaseNamingConvention(); + options.UseNpgsql(PostgresDatabaseFixture.DataSource!, o => o.UseNodaTime()).UseSnakeCaseNamingConvention(); }); // mock SMTP client - services.Replace(ServiceDescriptor.Transient(_ => new Mock().Object)); + services.Replace(ServiceDescriptor.Transient(_ => _mock.Object)); using IServiceScope scope = services.BuildServiceProvider().CreateScope(); ApplicationContext dbContext = @@ -131,23 +97,27 @@ private static void InitializeDatabase(ApplicationContext dbContext) } private static void Seed(ApplicationContext dbContext) { - Leaderboard leaderboard = - new() { Name = "Mario Goes to Jail", Slug = "mario-goes-to-jail" }; + if (!_seeded) + { + Leaderboard leaderboard = + new() { Name = "Mario Goes to Jail", Slug = "mario-goes-to-jail" }; - User admin = - new() - { - Id = TestInitCommonFields.Admin.Id, - Username = TestInitCommonFields.Admin.Username, - Email = TestInitCommonFields.Admin.Email, - Password = BCryptNet.EnhancedHashPassword(TestInitCommonFields.Admin.Password), - Role = UserRole.Administrator, - }; + User admin = + new() + { + Id = TestInitCommonFields.Admin.Id, + Username = TestInitCommonFields.Admin.Username, + Email = TestInitCommonFields.Admin.Email, + Password = BCryptNet.EnhancedHashPassword(TestInitCommonFields.Admin.Password), + Role = UserRole.Administrator, + }; - dbContext.Add(admin); - dbContext.Add(leaderboard); + dbContext.Add(admin); + dbContext.Add(leaderboard); - dbContext.SaveChanges(); + dbContext.SaveChanges(); + _seeded = true; + } } /// /// Deletes and recreates the database @@ -172,6 +142,7 @@ public async Task ResetDatabase() }); await respawner.ResetAsync(conn); + _seeded = false; InitializeDatabase(); } } From 6c990337e2777d935bf2a4cb140a9952d9e92feb Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:19:12 -0400 Subject: [PATCH 09/16] Await resetting the db. --- LeaderboardBackend.Test/Categories.cs | 2 +- LeaderboardBackend.Test/Features/Users/ConfirmAccountTests.cs | 4 ++-- LeaderboardBackend.Test/Features/Users/LoginTests.cs | 4 ++-- .../Features/Users/SendConfirmationTests.cs | 4 ++-- LeaderboardBackend.Test/Features/Users/SendRecoveryTests.cs | 4 ++-- LeaderboardBackend.Test/Leaderboards.cs | 2 +- LeaderboardBackend.Test/Runs.cs | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/LeaderboardBackend.Test/Categories.cs b/LeaderboardBackend.Test/Categories.cs index bb849d2a..adc2da3a 100644 --- a/LeaderboardBackend.Test/Categories.cs +++ b/LeaderboardBackend.Test/Categories.cs @@ -22,7 +22,7 @@ public async Task OneTimeSetUp() _factory = new TestApiFactory(); _apiClient = _factory.CreateTestApiClient(); - _factory.ResetDatabase(); + await _factory.ResetDatabase(); _jwt = (await _apiClient.LoginAdminUser()).Token; } diff --git a/LeaderboardBackend.Test/Features/Users/ConfirmAccountTests.cs b/LeaderboardBackend.Test/Features/Users/ConfirmAccountTests.cs index 7255e649..c9632663 100644 --- a/LeaderboardBackend.Test/Features/Users/ConfirmAccountTests.cs +++ b/LeaderboardBackend.Test/Features/Users/ConfirmAccountTests.cs @@ -35,9 +35,9 @@ public void Init() } [TearDown] - public void TearDown() + public async Task TearDown() { - _factory.ResetDatabase(); + await _factory.ResetDatabase(); _scope.Dispose(); } diff --git a/LeaderboardBackend.Test/Features/Users/LoginTests.cs b/LeaderboardBackend.Test/Features/Users/LoginTests.cs index 92f8de41..5af1aa66 100644 --- a/LeaderboardBackend.Test/Features/Users/LoginTests.cs +++ b/LeaderboardBackend.Test/Features/Users/LoginTests.cs @@ -20,9 +20,9 @@ namespace LeaderboardBackend.Test.Features.Users; public class LoginTests : IntegrationTestsBase { [OneTimeSetUp] - public void Init() + public async Task Init() { - _factory.ResetDatabase(); + await _factory.ResetDatabase(); // TODO: Swap to creating users via the UserService instead of calling the DB, once // it has the ability to change a user's roles. diff --git a/LeaderboardBackend.Test/Features/Users/SendConfirmationTests.cs b/LeaderboardBackend.Test/Features/Users/SendConfirmationTests.cs index 44b434d2..28693137 100644 --- a/LeaderboardBackend.Test/Features/Users/SendConfirmationTests.cs +++ b/LeaderboardBackend.Test/Features/Users/SendConfirmationTests.cs @@ -29,9 +29,9 @@ public void Init() } [TearDown] - public void TearDown() + public async Task TearDown() { - _factory.ResetDatabase(); + await _factory.ResetDatabase(); _scope.Dispose(); } diff --git a/LeaderboardBackend.Test/Features/Users/SendRecoveryTests.cs b/LeaderboardBackend.Test/Features/Users/SendRecoveryTests.cs index e722ad57..ce87d68a 100644 --- a/LeaderboardBackend.Test/Features/Users/SendRecoveryTests.cs +++ b/LeaderboardBackend.Test/Features/Users/SendRecoveryTests.cs @@ -28,9 +28,9 @@ public void Init() } [TearDown] - public void TearDown() + public async Task TearDown() { - _factory.ResetDatabase(); + await _factory.ResetDatabase(); _scope.Dispose(); } diff --git a/LeaderboardBackend.Test/Leaderboards.cs b/LeaderboardBackend.Test/Leaderboards.cs index f8f1621b..cc439365 100644 --- a/LeaderboardBackend.Test/Leaderboards.cs +++ b/LeaderboardBackend.Test/Leaderboards.cs @@ -30,7 +30,7 @@ public async Task OneTimeSetUp() _factory = new TestApiFactory(); _apiClient = _factory.CreateTestApiClient(); - _factory.ResetDatabase(); + await _factory.ResetDatabase(); _jwt = (await _apiClient.LoginAdminUser()).Token; } diff --git a/LeaderboardBackend.Test/Runs.cs b/LeaderboardBackend.Test/Runs.cs index 4a53123f..373c05df 100644 --- a/LeaderboardBackend.Test/Runs.cs +++ b/LeaderboardBackend.Test/Runs.cs @@ -28,7 +28,7 @@ public void OneTimeSetUp() [SetUp] public async Task SetUp() { - _factory.ResetDatabase(); + await _factory.ResetDatabase(); _jwt = (await _apiClient.LoginAdminUser()).Token; From 6483ca2210daf86e27bfecfe2b08facacd360a1b Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 16:32:44 -0400 Subject: [PATCH 10/16] Construct the data source outside of a lambda. --- LeaderboardBackend/Program.cs | 68 +++++++++++++++++------------------ 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index d29b8e59..cbe26647 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -60,43 +60,39 @@ .ValidateDataAnnotationsRecursively() .ValidateOnStart(); -builder.Services.AddDbContext( - (services, opt) => - { - ApplicationContextConfig appConfig = services - .GetRequiredService>() - .Value; - if (appConfig.Pg is not null) - { - PostgresConfig db = appConfig.Pg; - NpgsqlConnectionStringBuilder connectionBuilder = - new() - { - Host = db.Host, - Username = db.User, - Password = db.Password, - Database = db.Db, - IncludeErrorDetail = true, - }; - - if (db.Port is not null) - { - connectionBuilder.Port = db.Port.Value; - } +ApplicationContextConfig? appContextConfig = builder.Configuration.GetRequiredSection(ApplicationContextConfig.KEY).Get(); - NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); - dataSourceBuilder.UseNodaTime().MapEnum(); - opt.UseNpgsql(dataSourceBuilder.Build(), o => o.UseNodaTime()); - opt.UseSnakeCaseNamingConvention(); - } - else - { - throw new UnreachableException( - "The database configuration is invalid but it was not caught by validation!" - ); - } - } -); +if (appContextConfig is null || appContextConfig.Pg is null) +{ + throw new UnreachableException( + "The database configuration is invalid but it was not caught by validation!" + ); +} + +PostgresConfig db = appContextConfig.Pg; +NpgsqlConnectionStringBuilder connectionBuilder = new() +{ + Host = db.Host, + Username = db.User, + Password = db.Password, + Database = db.Db, + IncludeErrorDetail = true, +}; + +if (db.Port is not null) +{ + connectionBuilder.Port = db.Port.Value; +} + +NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); +dataSourceBuilder.UseNodaTime().MapEnum(); +NpgsqlDataSource dataSource = dataSourceBuilder.Build(); + +builder.Services.AddDbContext(opt => +{ + opt.UseNpgsql(dataSource, o => o.UseNodaTime()); + opt.UseSnakeCaseNamingConvention(); +}); // Add services to the container. builder.Services.AddScoped(); From 48c617e4ec87fafa163a98580c4048a14ecf39ac Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Wed, 11 Oct 2023 20:57:19 -0400 Subject: [PATCH 11/16] Don't require ApplicationContext config. --- LeaderboardBackend/Program.cs | 52 ++++++++++++++++------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index cbe26647..1f4f0f3e 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -60,39 +60,35 @@ .ValidateDataAnnotationsRecursively() .ValidateOnStart(); -ApplicationContextConfig? appContextConfig = builder.Configuration.GetRequiredSection(ApplicationContextConfig.KEY).Get(); +ApplicationContextConfig? appContextConfig = builder.Configuration.GetSection(ApplicationContextConfig.KEY).Get(); -if (appContextConfig is null || appContextConfig.Pg is null) +if (appContextConfig is not null && appContextConfig.Pg is not null) { - throw new UnreachableException( - "The database configuration is invalid but it was not caught by validation!" - ); -} + PostgresConfig db = appContextConfig.Pg; + NpgsqlConnectionStringBuilder connectionBuilder = new() + { + Host = db.Host, + Username = db.User, + Password = db.Password, + Database = db.Db, + IncludeErrorDetail = true, + }; -PostgresConfig db = appContextConfig.Pg; -NpgsqlConnectionStringBuilder connectionBuilder = new() -{ - Host = db.Host, - Username = db.User, - Password = db.Password, - Database = db.Db, - IncludeErrorDetail = true, -}; - -if (db.Port is not null) -{ - connectionBuilder.Port = db.Port.Value; -} + if (db.Port is not null) + { + connectionBuilder.Port = db.Port.Value; + } -NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); -dataSourceBuilder.UseNodaTime().MapEnum(); -NpgsqlDataSource dataSource = dataSourceBuilder.Build(); + NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); + dataSourceBuilder.UseNodaTime().MapEnum(); + NpgsqlDataSource dataSource = dataSourceBuilder.Build(); -builder.Services.AddDbContext(opt => -{ - opt.UseNpgsql(dataSource, o => o.UseNodaTime()); - opt.UseSnakeCaseNamingConvention(); -}); + builder.Services.AddDbContext(opt => + { + opt.UseNpgsql(dataSource, o => o.UseNodaTime()); + opt.UseSnakeCaseNamingConvention(); + }); +} // Add services to the container. builder.Services.AddScoped(); From 31892c8444378b2b05700a5c6cb813592df5e329 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:34:15 -0400 Subject: [PATCH 12/16] formatting --- LeaderboardBackend.Test/TestApi/TestApiFactory.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs index f5aaccd1..e1b6452a 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs @@ -119,6 +119,7 @@ private static void Seed(ApplicationContext dbContext) _seeded = true; } } + /// /// Deletes and recreates the database /// From 92467260603264725f5bce7e3f860bf4cd4ce306 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Fri, 13 Oct 2023 19:34:18 -0400 Subject: [PATCH 13/16] Don't nullcheck the config. --- LeaderboardBackend/Program.cs | 46 ++++++++++++++++------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/LeaderboardBackend/Program.cs b/LeaderboardBackend/Program.cs index 1f4f0f3e..8b07798e 100644 --- a/LeaderboardBackend/Program.cs +++ b/LeaderboardBackend/Program.cs @@ -60,35 +60,31 @@ .ValidateDataAnnotationsRecursively() .ValidateOnStart(); -ApplicationContextConfig? appContextConfig = builder.Configuration.GetSection(ApplicationContextConfig.KEY).Get(); +PostgresConfig db = builder.Configuration.GetSection(ApplicationContextConfig.KEY).Get()!.Pg!; -if (appContextConfig is not null && appContextConfig.Pg is not null) +NpgsqlConnectionStringBuilder connectionBuilder = new() { - PostgresConfig db = appContextConfig.Pg; - NpgsqlConnectionStringBuilder connectionBuilder = new() - { - Host = db.Host, - Username = db.User, - Password = db.Password, - Database = db.Db, - IncludeErrorDetail = true, - }; - - if (db.Port is not null) - { - connectionBuilder.Port = db.Port.Value; - } + Host = db.Host, + Username = db.User, + Password = db.Password, + Database = db.Db, + IncludeErrorDetail = true, +}; + +if (db.Port is not null) +{ + connectionBuilder.Port = db.Port.Value; +} - NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); - dataSourceBuilder.UseNodaTime().MapEnum(); - NpgsqlDataSource dataSource = dataSourceBuilder.Build(); +NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionBuilder.ConnectionString); +dataSourceBuilder.UseNodaTime().MapEnum(); +NpgsqlDataSource dataSource = dataSourceBuilder.Build(); - builder.Services.AddDbContext(opt => - { - opt.UseNpgsql(dataSource, o => o.UseNodaTime()); - opt.UseSnakeCaseNamingConvention(); - }); -} +builder.Services.AddDbContext(opt => +{ + opt.UseNpgsql(dataSource, o => o.UseNodaTime()); + opt.UseSnakeCaseNamingConvention(); +}); // Add services to the container. builder.Services.AddScoped(); From 5e581823ee4fc206bda770f3ebee608860419127 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Fri, 13 Oct 2023 19:37:06 -0400 Subject: [PATCH 14/16] Change environment variables for testing. --- .../TestApi/TestApiFactory.cs | 40 +++++-------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs index e1b6452a..7df40d58 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs @@ -29,39 +29,19 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) // Set the environment for the run to Staging builder.UseEnvironment(Environments.Staging); - base.ConfigureWebHost(builder); - - builder.ConfigureServices(services => + if (PostgresDatabaseFixture.PostgresContainer is null) { - if (PostgresDatabaseFixture.PostgresContainer is null) - { - throw new InvalidOperationException("Postgres container is not initialized."); - } - - ServiceDescriptor? dbContextDescriptor = services.SingleOrDefault( - d => d.ServiceType == - typeof(DbContextOptions)); - - if (dbContextDescriptor is not null) - { - services.Remove(dbContextDescriptor); - } + throw new InvalidOperationException("Postgres container is not initialized."); + } - services.Configure(conf => - conf.Pg = new PostgresConfig - { - Db = PostgresDatabaseFixture.Database!, - Port = (ushort)PostgresDatabaseFixture.Port, - Host = PostgresDatabaseFixture.PostgresContainer.Hostname, - User = PostgresDatabaseFixture.Username!, - Password = PostgresDatabaseFixture.Password! - }); - - services.AddDbContext(options => - { - options.UseNpgsql(PostgresDatabaseFixture.DataSource!, o => o.UseNodaTime()).UseSnakeCaseNamingConvention(); - }); + Environment.SetEnvironmentVariable("ApplicationContext__PG__DB", PostgresDatabaseFixture.Database); + Environment.SetEnvironmentVariable("ApplicationContext__PG__PORT", PostgresDatabaseFixture.Port.ToString()); + Environment.SetEnvironmentVariable("ApplicationContext__PG__HOST", PostgresDatabaseFixture.PostgresContainer!.Hostname); + Environment.SetEnvironmentVariable("ApplicationContext__PG__USER", PostgresDatabaseFixture.Username); + Environment.SetEnvironmentVariable("ApplicationContext__PG__PASSWORD", PostgresDatabaseFixture.Password); + builder.ConfigureServices(services => + { // mock SMTP client services.Replace(ServiceDescriptor.Transient(_ => _mock.Object)); From 5c08f090974a378b4d3ebbe8977287eeb7f854c7 Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Fri, 13 Oct 2023 19:39:00 -0400 Subject: [PATCH 15/16] Remove unnecessary static declarations. --- LeaderboardBackend.Test/TestApi/TestApiFactory.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs index 7df40d58..6eaec1fa 100644 --- a/LeaderboardBackend.Test/TestApi/TestApiFactory.cs +++ b/LeaderboardBackend.Test/TestApi/TestApiFactory.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Net.Http; using System.Threading.Tasks; using LeaderboardBackend.Models.Entities; @@ -7,7 +6,6 @@ using MailKit.Net.Smtp; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; @@ -65,7 +63,7 @@ public void InitializeDatabase() InitializeDatabase(dbContext); } - private static void InitializeDatabase(ApplicationContext dbContext) + private void InitializeDatabase(ApplicationContext dbContext) { if (!_migrated) { @@ -75,7 +73,7 @@ private static void InitializeDatabase(ApplicationContext dbContext) Seed(dbContext); } - private static void Seed(ApplicationContext dbContext) + private void Seed(ApplicationContext dbContext) { if (!_seeded) { From 611c4cefc87baafc5b924a731f7eb90187ec93fc Mon Sep 17 00:00:00 2001 From: Ted Wollman <25165500+TheTedder@users.noreply.github.com> Date: Sun, 15 Oct 2023 16:03:34 -0400 Subject: [PATCH 16/16] Remove unused template db connection. --- LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs b/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs index 2c628520..57e7a640 100644 --- a/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs +++ b/LeaderboardBackend.Test/Fixtures/PostgresDatabaseFixture.cs @@ -1,6 +1,4 @@ -using System; using System.Threading.Tasks; -using LeaderboardBackend.Models.Entities; using Npgsql; using NUnit.Framework; using Testcontainers.PostgreSql; @@ -15,7 +13,6 @@ public class PostgresDatabaseFixture public static string? Database { get; private set; } public static string? Username { get; private set; } public static string? Password { get; private set; } - public static NpgsqlDataSource? DataSource { get; private set; } public static int Port { get; private set; } [OneTimeSetUp] @@ -31,9 +28,6 @@ public static async Task OneTimeSetup() Password = connStrBuilder.Password!; Database = connStrBuilder.Database!; Port = connStrBuilder.Port; - NpgsqlDataSourceBuilder dataSourceBuilder = new(connStrBuilder.ConnectionString); - dataSourceBuilder.UseNodaTime().MapEnum(); - DataSource = dataSourceBuilder.Build(); } [OneTimeTearDown]