diff --git a/.editorconfig b/.editorconfig index becec003..54da9070 100644 --- a/.editorconfig +++ b/.editorconfig @@ -381,5 +381,5 @@ roslynator_binary_operator_new_line = before roslynator_conditional_operator_new_line = before roslynator_null_conditional_operator_new_line = before -[*.{json,env,yml,yaml,xml,xsd,html,cshtml,csproj,props,sln,resx}] +[*.{json,env,yml,yaml,xml,xsd,html,cshtml,csproj,props,sln,slnx,resx}] indent_size = 2 diff --git a/Hikkaba.Tests.Integration/Models/IAppFactorySeedResult.cs b/Hikkaba.Tests.Integration/Models/IAppFactorySeedResult.cs new file mode 100644 index 00000000..39d5429c --- /dev/null +++ b/Hikkaba.Tests.Integration/Models/IAppFactorySeedResult.cs @@ -0,0 +1,10 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Hikkaba.Tests.Integration.Models; + +internal interface IAppFactorySeedResult : IDisposable +{ + IServiceScope Scope { get; set; } + CustomAppFactory AppFactory { get; set; } +} diff --git a/Hikkaba.Tests.Integration/Models/ISeedResult.cs b/Hikkaba.Tests.Integration/Models/ISeedResult.cs new file mode 100644 index 00000000..542951b2 --- /dev/null +++ b/Hikkaba.Tests.Integration/Models/ISeedResult.cs @@ -0,0 +1,9 @@ +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace Hikkaba.Tests.Integration.Models; + +internal interface ISeedResult : IDisposable +{ + public IServiceScope Scope { get; set; } +} diff --git a/Hikkaba.Tests.Integration/Models/SeedResult.cs b/Hikkaba.Tests.Integration/Models/SeedResult.cs new file mode 100644 index 00000000..4e8d0731 --- /dev/null +++ b/Hikkaba.Tests.Integration/Models/SeedResult.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Hikkaba.Tests.Integration.Models; + +internal sealed class SeedResult : ISeedResult, IAppFactorySeedResult +{ + public required IServiceScope Scope { get; set; } + public required CustomAppFactory AppFactory { get; set; } + + public void Dispose() + { + Scope.Dispose(); + AppFactory.Dispose(); + } +} diff --git a/Hikkaba.Tests.Integration/Tests/Repositories/BanRepositoryTests.cs b/Hikkaba.Tests.Integration/Tests/Repositories/BanRepositoryTests.cs index 4b6b1cda..01979c9e 100644 --- a/Hikkaba.Tests.Integration/Tests/Repositories/BanRepositoryTests.cs +++ b/Hikkaba.Tests.Integration/Tests/Repositories/BanRepositoryTests.cs @@ -14,6 +14,7 @@ using Hikkaba.Shared.Enums; using Hikkaba.Tests.Integration.Constants; using Hikkaba.Tests.Integration.Extensions; +using Hikkaba.Tests.Integration.Models; using Hikkaba.Tests.Integration.Services; using Hikkaba.Tests.Integration.Utils; using JetBrains.Annotations; @@ -44,10 +45,24 @@ public async Task OneTimeTearDownAsync() } [MustDisposeResource] - private async Task CreateAppFactoryAsync() + private async Task Seed(CancellationToken cancellationToken) { var connectionString = await _contextManager!.CreateRespawnedDbConnectionStringAsync(); - return new CustomAppFactory(connectionString); + var customAppFactory = new CustomAppFactory(connectionString); + + var scope = customAppFactory.Services.GetRequiredService().CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + if ((await dbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) + { + await dbContext.Database.MigrateAsync(cancellationToken); + } + + return new SeedResult + { + Scope = scope, + AppFactory = customAppFactory, + }; } [CancelAfter(TestDefaults.TestTimeout)] @@ -61,16 +76,11 @@ public async Task ListBansPaginatedAsync_WhenSearchExact_ReturnsExpectedResult( CancellationToken cancellationToken) { // Arrange - await using var customAppFactory = await CreateAppFactoryAsync(); - using var scope = customAppFactory.Services.GetRequiredService().CreateScope(); - var timeProvider = scope.ServiceProvider.GetRequiredService(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var hashService = scope.ServiceProvider.GetRequiredService(); + using var seedResult = await Seed(cancellationToken); - if ((await dbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) - { - await dbContext.Database.MigrateAsync(cancellationToken); - } + var dbContext = seedResult.Scope.ServiceProvider.GetRequiredService(); + var hashService = seedResult.Scope.ServiceProvider.GetRequiredService(); + var timeProvider = seedResult.Scope.ServiceProvider.GetRequiredService(); // Seed var admin = new ApplicationUser @@ -168,7 +178,7 @@ public async Task ListBansPaginatedAsync_WhenSearchExact_ReturnsExpectedResult( await dbContext.SaveChangesAsync(cancellationToken); - var repository = scope.ServiceProvider.GetRequiredService(); + var repository = seedResult.Scope.ServiceProvider.GetRequiredService(); // Act var result = await repository.ListBansPaginatedAsync(new BanPagingFilter @@ -195,16 +205,10 @@ public async Task ListBansPaginatedAsync_WhenSearchInRange_ReturnsExpectedResult CancellationToken cancellationToken) { // Arrange - await using var customAppFactory = await CreateAppFactoryAsync(); - using var scope = customAppFactory.Services.GetRequiredService().CreateScope(); - var timeProvider = scope.ServiceProvider.GetRequiredService(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var hashService = scope.ServiceProvider.GetRequiredService(); - - if ((await dbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) - { - await dbContext.Database.MigrateAsync(cancellationToken); - } + using var seedResult = await Seed(cancellationToken); + var dbContext = seedResult.Scope.ServiceProvider.GetRequiredService(); + var hashService = seedResult.Scope.ServiceProvider.GetRequiredService(); + var timeProvider = seedResult.Scope.ServiceProvider.GetRequiredService(); // Seed var admin = new ApplicationUser @@ -306,7 +310,7 @@ public async Task ListBansPaginatedAsync_WhenSearchInRange_ReturnsExpectedResult await dbContext.SaveChangesAsync(cancellationToken); - var repository = scope.ServiceProvider.GetRequiredService(); + var repository = seedResult.Scope.ServiceProvider.GetRequiredService(); // Act var result = await repository.ListBansPaginatedAsync(new BanPagingFilter diff --git a/Hikkaba.Tests.Integration/Tests/Repositories/PostRepositoryTests.cs b/Hikkaba.Tests.Integration/Tests/Repositories/PostRepositoryTests.cs index 87347b50..c80e28f4 100644 --- a/Hikkaba.Tests.Integration/Tests/Repositories/PostRepositoryTests.cs +++ b/Hikkaba.Tests.Integration/Tests/Repositories/PostRepositoryTests.cs @@ -14,6 +14,7 @@ using Hikkaba.Shared.Constants; using Hikkaba.Tests.Integration.Constants; using Hikkaba.Tests.Integration.Extensions; +using Hikkaba.Tests.Integration.Models; using Hikkaba.Tests.Integration.Services; using Hikkaba.Tests.Integration.Utils; using JetBrains.Annotations; @@ -44,10 +45,24 @@ public async Task OneTimeTearDownAsync() } [MustDisposeResource] - private async Task CreateAppFactoryAsync() + private async Task Seed(CancellationToken cancellationToken) { var connectionString = await _contextManager!.CreateRespawnedDbConnectionStringAsync(); - return new CustomAppFactory(connectionString); + var customAppFactory = new CustomAppFactory(connectionString); + + var scope = customAppFactory.Services.GetRequiredService().CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + if ((await dbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) + { + await dbContext.Database.MigrateAsync(cancellationToken); + } + + return new SeedResult + { + Scope = scope, + AppFactory = customAppFactory, + }; } [CancelAfter(TestDefaults.TestTimeout)] @@ -63,17 +78,11 @@ public async Task SearchPostsPaginatedAsync_WhenSearchQueryIsProvided_ReturnsExp CancellationToken cancellationToken) { // Arrange - await using var customAppFactory = await CreateAppFactoryAsync(); - using var scope = customAppFactory.Services.GetRequiredService().CreateScope(); - var timeProvider = scope.ServiceProvider.GetRequiredService(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var hashService = scope.ServiceProvider.GetRequiredService(); - var logger = scope.ServiceProvider.GetRequiredService>(); - - if ((await dbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) - { - await dbContext.Database.MigrateAsync(cancellationToken); - } + using var seedResult = await Seed(cancellationToken); + var dbContext = seedResult.Scope.ServiceProvider.GetRequiredService(); + var hashService = seedResult.Scope.ServiceProvider.GetRequiredService(); + var timeProvider = seedResult.Scope.ServiceProvider.GetRequiredService(); + var logger = seedResult.Scope.ServiceProvider.GetRequiredService>(); // Seed var admin = new ApplicationUser @@ -165,7 +174,7 @@ public async Task SearchPostsPaginatedAsync_WhenSearchQueryIsProvided_ReturnsExp await DbUtils.WaitForFulltextIndexAsync(logger, dbContext, ["Posts", "Threads"], cancellationToken: cancellationToken); - var repository = scope.ServiceProvider.GetRequiredService(); + var repository = seedResult.Scope.ServiceProvider.GetRequiredService(); // Act var result = await repository.SearchPostsPaginatedAsync(new SearchPostsPagingFilter diff --git a/Hikkaba.Tests.Integration/Tests/Repositories/ThreadRepositoryTests.cs b/Hikkaba.Tests.Integration/Tests/Repositories/ThreadRepositoryTests.cs index 00ad48eb..e216b616 100644 --- a/Hikkaba.Tests.Integration/Tests/Repositories/ThreadRepositoryTests.cs +++ b/Hikkaba.Tests.Integration/Tests/Repositories/ThreadRepositoryTests.cs @@ -17,6 +17,7 @@ using Hikkaba.Shared.Constants; using Hikkaba.Tests.Integration.Constants; using Hikkaba.Tests.Integration.Extensions; +using Hikkaba.Tests.Integration.Models; using Hikkaba.Tests.Integration.Services; using Hikkaba.Tests.Integration.Utils; using JetBrains.Annotations; @@ -47,10 +48,24 @@ public async Task OneTimeTearDownAsync() } [MustDisposeResource] - private async Task CreateAppFactoryAsync() + private async Task Seed(CancellationToken cancellationToken) { var connectionString = await _contextManager!.CreateRespawnedDbConnectionStringAsync(); - return new CustomAppFactory(connectionString); + var customAppFactory = new CustomAppFactory(connectionString); + + var scope = customAppFactory.Services.GetRequiredService().CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + if ((await dbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) + { + await dbContext.Database.MigrateAsync(cancellationToken); + } + + return new SeedResult + { + Scope = scope, + AppFactory = customAppFactory, + }; } [CancelAfter(TestDefaults.TestTimeout)] @@ -59,16 +74,10 @@ public async Task ListThreadPreviewsPaginatedAsync_WhenOnePageExists_ReturnsCorr CancellationToken cancellationToken) { // Arrange - await using var customAppFactory = await CreateAppFactoryAsync(); - using var scope = customAppFactory.Services.GetRequiredService().CreateScope(); - var timeProvider = scope.ServiceProvider.GetRequiredService(); - var dbContext = scope.ServiceProvider.GetRequiredService(); - var hashService = scope.ServiceProvider.GetRequiredService(); - - if ((await dbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) - { - await dbContext.Database.MigrateAsync(cancellationToken); - } + using var seedResult = await Seed(cancellationToken); + var dbContext = seedResult.Scope.ServiceProvider.GetRequiredService(); + var hashService = seedResult.Scope.ServiceProvider.GetRequiredService(); + var timeProvider = seedResult.Scope.ServiceProvider.GetRequiredService(); // Seed var admin = new ApplicationUser @@ -235,7 +244,7 @@ public async Task ListThreadPreviewsPaginatedAsync_WhenOnePageExists_ReturnsCorr await dbContext.SaveChangesAsync(cancellationToken); - var repository = scope.ServiceProvider.GetRequiredService(); + var repository = seedResult.Scope.ServiceProvider.GetRequiredService(); // Act var result = await repository.ListThreadPreviewsPaginatedAsync(new ThreadPreviewFilter @@ -307,19 +316,15 @@ public async Task ListThreadPreviewsPaginatedAsync_WhenManyPagesExist_ReturnsCor const int totalPostCountPerThread = 55; // Arrange - await using var customAppFactory = await CreateAppFactoryAsync(); - using (var seedScope = customAppFactory.Services.GetRequiredService().CreateScope()) + using var seedResult = await Seed(cancellationToken); + + using (var seedScope = seedResult.Scope.ServiceProvider.GetRequiredService().CreateScope()) { var seedTimeProvider = seedScope.ServiceProvider.GetRequiredService(); var seedDbContext = seedScope.ServiceProvider.GetRequiredService(); var hashService = seedScope.ServiceProvider.GetRequiredService(); var timeProvider = seedScope.ServiceProvider.GetRequiredService(); - if ((await seedDbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) - { - await seedDbContext.Database.MigrateAsync(cancellationToken); - } - // Seed var admin = new ApplicationUser { @@ -487,7 +492,7 @@ public async Task ListThreadPreviewsPaginatedAsync_WhenManyPagesExist_ReturnsCor } // Act - using (var actScope = customAppFactory.Services.GetRequiredService().CreateScope()) + using (var actScope = seedResult.Scope.ServiceProvider.GetRequiredService().CreateScope()) { var threadRepository = actScope.ServiceProvider.GetRequiredService(); @@ -569,18 +574,14 @@ public async Task ListThreadPreviewsPaginatedAsync_WhenPinnedThreadExist_Returns const int totalPostCountPerThread = 25; // Arrange - await using var customAppFactory = await CreateAppFactoryAsync(); - using (var seedScope = customAppFactory.Services.GetRequiredService().CreateScope()) + using var seedResult = await Seed(cancellationToken); + + using (var seedScope = seedResult.Scope.ServiceProvider.GetRequiredService().CreateScope()) { - var seedTimeProvider = customAppFactory.Services.GetRequiredService(); + var seedTimeProvider = seedScope.ServiceProvider.GetRequiredService(); var seedDbContext = seedScope.ServiceProvider.GetRequiredService(); var hashService = seedScope.ServiceProvider.GetRequiredService(); - if ((await seedDbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) - { - await seedDbContext.Database.MigrateAsync(cancellationToken); - } - // Seed var admin = new ApplicationUser { @@ -747,7 +748,7 @@ public async Task ListThreadPreviewsPaginatedAsync_WhenPinnedThreadExist_Returns } // Act - using (var actScope = customAppFactory.Services.GetRequiredService().CreateScope()) + using (var actScope = seedResult.Scope.ServiceProvider.GetRequiredService().CreateScope()) { var threadRepository = actScope.ServiceProvider.GetRequiredService(); var actualThreadPreviews = await threadRepository.ListThreadPreviewsPaginatedAsync(new ThreadPreviewFilter @@ -834,18 +835,14 @@ public async Task ListThreadPreviewsPaginatedAsync_WhenSagePostExist_ReturnsCorr const int totalPostCountPerThread = 25; // Arrange - await using var customAppFactory = await CreateAppFactoryAsync(); - using (var seedScope = customAppFactory.Services.GetRequiredService().CreateScope()) + using var seedResult = await Seed(cancellationToken); + + using (var seedScope = seedResult.Scope.ServiceProvider.GetRequiredService().CreateScope()) { var seedTimeProvider = seedScope.ServiceProvider.GetRequiredService(); var seedDbContext = seedScope.ServiceProvider.GetRequiredService(); var hashService = seedScope.ServiceProvider.GetRequiredService(); - if ((await seedDbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) - { - await seedDbContext.Database.MigrateAsync(cancellationToken); - } - // Seed var admin = new ApplicationUser { @@ -1041,7 +1038,7 @@ public async Task ListThreadPreviewsPaginatedAsync_WhenSagePostExist_ReturnsCorr } // Act - using (var actScope = customAppFactory.Services.GetRequiredService().CreateScope()) + using (var actScope = seedResult.Scope.ServiceProvider.GetRequiredService().CreateScope()) { var threadRepository = actScope.ServiceProvider.GetRequiredService(); var actualThreadPreviews = await threadRepository.ListThreadPreviewsPaginatedAsync(new ThreadPreviewFilter @@ -1120,232 +1117,232 @@ void AddPosts(Thread thread, DateTime startingAt, int count, bool isSageEnabled, const int pageSize = 10; // Arrange - await using var customAppFactory = await CreateAppFactoryAsync(); - using var seedScope = customAppFactory.Services.GetRequiredService().CreateScope(); - var seedTimeProvider = seedScope.ServiceProvider.GetRequiredService(); - var seedDbContext = seedScope.ServiceProvider.GetRequiredService(); - var hashService = seedScope.ServiceProvider.GetRequiredService(); - - if ((await seedDbContext.Database.GetPendingMigrationsAsync(cancellationToken)).Any()) - { - await seedDbContext.Database.MigrateAsync(cancellationToken); - } - - // Seed - var admin = new ApplicationUser - { - UserName = "admin", - NormalizedUserName = "ADMIN", - Email = "admin@example.com", - NormalizedEmail = "ADMIN@EXAMPLE.COM", - EmailConfirmed = true, - SecurityStamp = "896e8014-c237-41f5-a925-dabf640ee4c4", - ConcurrencyStamp = "43035b63-359d-4c23-8812-29bbc5affbf2", - CreatedAt = seedTimeProvider.GetUtcNow().UtcDateTime, - }; - seedDbContext.Users.Add(admin); - - var category1 = new Category - { - IsDeleted = false, - CreatedAt = seedTimeProvider.GetUtcNow().UtcDateTime, - ModifiedAt = null, - Alias = "b", - Name = "Random Foo", - IsHidden = false, - DefaultBumpLimit = 500, - ShowThreadLocalUserHash = false, - ShowOs = false, - ShowBrowser = false, - ShowCountry = false, - MaxThreadCount = Defaults.MaxThreadCountInCategory, - CreatedBy = admin, - }; - var category2 = new Category - { - IsDeleted = false, - CreatedAt = seedTimeProvider.GetUtcNow().UtcDateTime, - ModifiedAt = null, - Alias = "a", - Name = "Random Bar", - IsHidden = false, - DefaultBumpLimit = 500, - ShowThreadLocalUserHash = false, - ShowOs = false, - ShowBrowser = false, - ShowCountry = false, - MaxThreadCount = Defaults.MaxThreadCountInCategory, - CreatedBy = admin, - }; - seedDbContext.Categories.AddRange(category1, category2); + using var seedResult = await Seed(cancellationToken); - var salt1 = GuidGenerator.GenerateSeededGuid(); - var salt2 = GuidGenerator.GenerateSeededGuid(); - var utcNow = seedTimeProvider.GetUtcNow().UtcDateTime; - var deletedThread = new Thread - { - CreatedAt = utcNow, - LastBumpAt = utcNow, - Title = "deleted thread", - IsPinned = true, - IsClosed = true, - IsDeleted = true, - BumpLimit = 500, - Salt = salt1, - Category = category1, - Posts = - [ - new Post - { - IsOriginalPost = true, - BlobContainerId = new Guid("F115A07E-3B7F-4F54-8140-A9481EBE3F0A"), - CreatedAt = seedTimeProvider.GetUtcNow().UtcDateTime, - IsSageEnabled = false, - IsDeleted = false, - MessageText = $"test post in deleted thread", - MessageHtml = $"test post in deleted thread", - UserIpAddress = IPAddress.Parse($"127.0.0.1").GetAddressBytes(), - UserAgent = "Firefox", - ThreadLocalUserHash = hashService.GetHashBytes(salt1, IPAddress.Parse($"127.0.0.1").GetAddressBytes()), - }, - ], - }; - var anotherCategoryThread = new Thread - { - CreatedAt = utcNow, - LastBumpAt = utcNow, - Title = "another category thread", - IsPinned = false, - IsClosed = false, - IsDeleted = false, - BumpLimit = 500, - Salt = salt2, - Category = category2, - Posts = - [ - new Post - { - IsOriginalPost = true, - BlobContainerId = new Guid("A4129657-90E4-4B5C-95A6-CB9D1B9746EC"), - CreatedAt = seedTimeProvider.GetUtcNow().UtcDateTime, - IsSageEnabled = false, - IsDeleted = false, - MessageText = $"test post in deleted thread", - MessageHtml = $"test post in deleted thread", - UserIpAddress = IPAddress.Parse($"127.0.0.1").GetAddressBytes(), - UserAgent = "Firefox", - ThreadLocalUserHash = hashService.GetHashBytes(salt1, IPAddress.Parse($"127.0.0.1").GetAddressBytes()), - }, - ], - }; - var thread1 = new Thread - { - CreatedAt = utcNow.AddMinutes(1), - LastBumpAt = utcNow.AddMinutes(1), - Title = "thread with bump limit 1", - BumpLimit = bumpLimit, - Salt = GuidGenerator.GenerateSeededGuid(), - Category = category1, - }; - var thread2 = new Thread + using (var seedScope = seedResult.Scope.ServiceProvider.GetRequiredService().CreateScope()) { - CreatedAt = utcNow.AddMinutes(2), - LastBumpAt = utcNow.AddMinutes(2), - Title = "thread with bump limit 2", - BumpLimit = bumpLimit, - Salt = GuidGenerator.GenerateSeededGuid(), - Category = category1, - }; - var thread3 = new Thread - { - CreatedAt = utcNow.AddMinutes(3), - LastBumpAt = utcNow.AddMinutes(3), - Title = "thread with bump limit 3", - BumpLimit = bumpLimit, - Salt = GuidGenerator.GenerateSeededGuid(), - Category = category1, - }; - var thread4 = new Thread - { - CreatedAt = utcNow.AddMinutes(4), - LastBumpAt = utcNow.AddMinutes(4), - Title = "thread with bump limit 4", - BumpLimit = bumpLimit, - Salt = GuidGenerator.GenerateSeededGuid(), - Category = category1, - }; - List allThreads = [deletedThread, anotherCategoryThread, thread1, thread2, thread3, thread4]; - seedDbContext.Threads.AddRange(allThreads); - - // these threads contain the newest posts, but they aren't included in our query - AddPosts(deletedThread, seedTimeProvider.GetUtcNow().UtcDateTime.AddYears(2), bumpLimit + 2, false, false, hashService); - AddPosts(anotherCategoryThread, seedTimeProvider.GetUtcNow().UtcDateTime.AddYears(2), bumpLimit + 2, false, false, hashService); - - // thread1 contains several old posts before bump limit and several new posts after bump limit, which shouldn't affect the result - AddPosts(thread1, seedTimeProvider.GetUtcNow().UtcDateTime.AddYears(-1), bumpLimit, false, false, hashService); - AddPosts(thread1, seedTimeProvider.GetUtcNow().UtcDateTime.AddYears(1), 2, false, false, hashService); - thread1.LastBumpAt = thread1.Posts.Where(p => !p.IsSageEnabled).Max(x => x.CreatedAt); - - // thread2 contains several new posts - AddPosts(thread2, seedTimeProvider.GetUtcNow().UtcDateTime.AddDays(1).AddHours(1), 1, true, false, hashService); - AddPosts(thread2, seedTimeProvider.GetUtcNow().UtcDateTime.AddDays(1), bumpLimit, false, false, hashService); - thread2.LastBumpAt = thread2.Posts.Where(p => !p.IsSageEnabled).Max(x => x.CreatedAt); + var dbContext = seedScope.ServiceProvider.GetRequiredService(); + var hashService = seedScope.ServiceProvider.GetRequiredService(); + var timeProvider = seedScope.ServiceProvider.GetRequiredService(); - // thread3 contains even newer posts - AddPosts(thread3, seedTimeProvider.GetUtcNow().UtcDateTime.AddDays(1).AddHours(3), 1, true, false, hashService); - AddPosts(thread3, seedTimeProvider.GetUtcNow().UtcDateTime.AddDays(1).AddSeconds(1), bumpLimit, false, false, hashService); - thread3.LastBumpAt = thread3.Posts.Where(p => !p.IsSageEnabled).Max(x => x.CreatedAt); + // Seed + var admin = new ApplicationUser + { + UserName = "admin", + NormalizedUserName = "ADMIN", + Email = "admin@example.com", + NormalizedEmail = "ADMIN@EXAMPLE.COM", + EmailConfirmed = true, + SecurityStamp = "896e8014-c237-41f5-a925-dabf640ee4c4", + ConcurrencyStamp = "43035b63-359d-4c23-8812-29bbc5affbf2", + CreatedAt = timeProvider.GetUtcNow().UtcDateTime, + }; + dbContext.Users.Add(admin); - // thread4 contains a lot of posts - AddPosts(thread4, seedTimeProvider.GetUtcNow().UtcDateTime, bumpLimit + 10, false, false, hashService); - AddPosts(thread4, seedTimeProvider.GetUtcNow().UtcDateTime.AddYears(5), bumpLimit + 10, false, false, hashService); - thread4.LastBumpAt = thread4.Posts.Where(p => !p.IsSageEnabled).Max(x => x.CreatedAt); + var category1 = new Category + { + IsDeleted = false, + CreatedAt = timeProvider.GetUtcNow().UtcDateTime, + ModifiedAt = null, + Alias = "b", + Name = "Random Foo", + IsHidden = false, + DefaultBumpLimit = 500, + ShowThreadLocalUserHash = false, + ShowOs = false, + ShowBrowser = false, + ShowCountry = false, + MaxThreadCount = Defaults.MaxThreadCountInCategory, + CreatedBy = admin, + }; + var category2 = new Category + { + IsDeleted = false, + CreatedAt = timeProvider.GetUtcNow().UtcDateTime, + ModifiedAt = null, + Alias = "a", + Name = "Random Bar", + IsHidden = false, + DefaultBumpLimit = 500, + ShowThreadLocalUserHash = false, + ShowOs = false, + ShowBrowser = false, + ShowCountry = false, + MaxThreadCount = Defaults.MaxThreadCountInCategory, + CreatedBy = admin, + }; + dbContext.Categories.AddRange(category1, category2); - await seedDbContext.SaveChangesAsync(cancellationToken); + var salt1 = GuidGenerator.GenerateSeededGuid(); + var salt2 = GuidGenerator.GenerateSeededGuid(); + var utcNow = timeProvider.GetUtcNow().UtcDateTime; + var deletedThread = new Thread + { + CreatedAt = utcNow, + LastBumpAt = utcNow, + Title = "deleted thread", + IsPinned = true, + IsClosed = true, + IsDeleted = true, + BumpLimit = 500, + Salt = salt1, + Category = category1, + Posts = + [ + new Post + { + IsOriginalPost = true, + BlobContainerId = new Guid("F115A07E-3B7F-4F54-8140-A9481EBE3F0A"), + CreatedAt = timeProvider.GetUtcNow().UtcDateTime, + IsSageEnabled = false, + IsDeleted = false, + MessageText = $"test post in deleted thread", + MessageHtml = $"test post in deleted thread", + UserIpAddress = IPAddress.Parse($"127.0.0.1").GetAddressBytes(), + UserAgent = "Firefox", + ThreadLocalUserHash = hashService.GetHashBytes(salt1, IPAddress.Parse($"127.0.0.1").GetAddressBytes()), + }, + ], + }; + var anotherCategoryThread = new Thread + { + CreatedAt = utcNow, + LastBumpAt = utcNow, + Title = "another category thread", + IsPinned = false, + IsClosed = false, + IsDeleted = false, + BumpLimit = 500, + Salt = salt2, + Category = category2, + Posts = + [ + new Post + { + IsOriginalPost = true, + BlobContainerId = new Guid("A4129657-90E4-4B5C-95A6-CB9D1B9746EC"), + CreatedAt = timeProvider.GetUtcNow().UtcDateTime, + IsSageEnabled = false, + IsDeleted = false, + MessageText = $"test post in deleted thread", + MessageHtml = $"test post in deleted thread", + UserIpAddress = IPAddress.Parse($"127.0.0.1").GetAddressBytes(), + UserAgent = "Firefox", + ThreadLocalUserHash = hashService.GetHashBytes(salt1, IPAddress.Parse($"127.0.0.1").GetAddressBytes()), + }, + ], + }; + var thread1 = new Thread + { + CreatedAt = utcNow.AddMinutes(1), + LastBumpAt = utcNow.AddMinutes(1), + Title = "thread with bump limit 1", + BumpLimit = bumpLimit, + Salt = GuidGenerator.GenerateSeededGuid(), + Category = category1, + }; + var thread2 = new Thread + { + CreatedAt = utcNow.AddMinutes(2), + LastBumpAt = utcNow.AddMinutes(2), + Title = "thread with bump limit 2", + BumpLimit = bumpLimit, + Salt = GuidGenerator.GenerateSeededGuid(), + Category = category1, + }; + var thread3 = new Thread + { + CreatedAt = utcNow.AddMinutes(3), + LastBumpAt = utcNow.AddMinutes(3), + Title = "thread with bump limit 3", + BumpLimit = bumpLimit, + Salt = GuidGenerator.GenerateSeededGuid(), + Category = category1, + }; + var thread4 = new Thread + { + CreatedAt = utcNow.AddMinutes(4), + LastBumpAt = utcNow.AddMinutes(4), + Title = "thread with bump limit 4", + BumpLimit = bumpLimit, + Salt = GuidGenerator.GenerateSeededGuid(), + Category = category1, + }; + List allThreads = [deletedThread, anotherCategoryThread, thread1, thread2, thread3, thread4]; + dbContext.Threads.AddRange(allThreads); + + // these threads contain the newest posts, but they aren't included in our query + AddPosts(deletedThread, timeProvider.GetUtcNow().UtcDateTime.AddYears(2), bumpLimit + 2, false, false, hashService); + AddPosts(anotherCategoryThread, timeProvider.GetUtcNow().UtcDateTime.AddYears(2), bumpLimit + 2, false, false, hashService); + + // thread1 contains several old posts before bump limit and several new posts after bump limit, which shouldn't affect the result + AddPosts(thread1, timeProvider.GetUtcNow().UtcDateTime.AddYears(-1), bumpLimit, false, false, hashService); + AddPosts(thread1, timeProvider.GetUtcNow().UtcDateTime.AddYears(1), 2, false, false, hashService); + thread1.LastBumpAt = thread1.Posts.Where(p => !p.IsSageEnabled).Max(x => x.CreatedAt); + + // thread2 contains several new posts + AddPosts(thread2, timeProvider.GetUtcNow().UtcDateTime.AddDays(1).AddHours(1), 1, true, false, hashService); + AddPosts(thread2, timeProvider.GetUtcNow().UtcDateTime.AddDays(1), bumpLimit, false, false, hashService); + thread2.LastBumpAt = thread2.Posts.Where(p => !p.IsSageEnabled).Max(x => x.CreatedAt); + + // thread3 contains even newer posts + AddPosts(thread3, timeProvider.GetUtcNow().UtcDateTime.AddDays(1).AddHours(3), 1, true, false, hashService); + AddPosts(thread3, timeProvider.GetUtcNow().UtcDateTime.AddDays(1).AddSeconds(1), bumpLimit, false, false, hashService); + thread3.LastBumpAt = thread3.Posts.Where(p => !p.IsSageEnabled).Max(x => x.CreatedAt); + + // thread4 contains a lot of posts + AddPosts(thread4, timeProvider.GetUtcNow().UtcDateTime, bumpLimit + 10, false, false, hashService); + AddPosts(thread4, timeProvider.GetUtcNow().UtcDateTime.AddYears(5), bumpLimit + 10, false, false, hashService); + thread4.LastBumpAt = thread4.Posts.Where(p => !p.IsSageEnabled).Max(x => x.CreatedAt); + + await dbContext.SaveChangesAsync(cancellationToken); + } // Act - using var actScope = customAppFactory.Services.GetRequiredService().CreateScope(); - var threadRepository = actScope.ServiceProvider.GetRequiredService(); - var actualThreadPreviews = await threadRepository.ListThreadPreviewsPaginatedAsync(new ThreadPreviewFilter + using (var actScope = seedResult.Scope.ServiceProvider.GetRequiredService().CreateScope()) { - PageNumber = pageNumber, - PageSize = pageSize, - OrderBy = - [ - new OrderByItem { Field = nameof(ThreadPreviewModel.IsPinned), Direction = OrderByDirection.Desc }, - new OrderByItem { Field = nameof(ThreadPreviewModel.LastBumpAt), Direction = OrderByDirection.Desc }, - new OrderByItem { Field = nameof(ThreadPreviewModel.Id), Direction = OrderByDirection.Desc }, - ], - CategoryAlias = "b", - IncludeDeleted = false, - }, cancellationToken); + var threadRepository = actScope.ServiceProvider.GetRequiredService(); + var actualThreadPreviews = await threadRepository.ListThreadPreviewsPaginatedAsync(new ThreadPreviewFilter + { + PageNumber = pageNumber, + PageSize = pageSize, + OrderBy = + [ + new OrderByItem { Field = nameof(ThreadPreviewModel.IsPinned), Direction = OrderByDirection.Desc }, + new OrderByItem { Field = nameof(ThreadPreviewModel.LastBumpAt), Direction = OrderByDirection.Desc }, + new OrderByItem { Field = nameof(ThreadPreviewModel.Id), Direction = OrderByDirection.Desc }, + ], + CategoryAlias = "b", + IncludeDeleted = false, + }, cancellationToken); - // Assert - Assert.That(actualThreadPreviews, Is.Not.Null); + // Assert + Assert.That(actualThreadPreviews, Is.Not.Null); - Assert.That(actualThreadPreviews.Data, Has.Count.EqualTo(4)); + Assert.That(actualThreadPreviews.Data, Has.Count.EqualTo(4)); - // check that category is correct - Assert.That(actualThreadPreviews.Data, Is.All.Matches(x => x.CategoryAlias == "b")); + // check that category is correct + Assert.That(actualThreadPreviews.Data, Is.All.Matches(x => x.CategoryAlias == "b")); - // check that there are no deleted threads - Assert.That(actualThreadPreviews.Data, Is.All.Matches(x => !x.IsDeleted)); + // check that there are no deleted threads + Assert.That(actualThreadPreviews.Data, Is.All.Matches(x => !x.IsDeleted)); - // check that there are no deleted posts - Assert.That(actualThreadPreviews.Data, Is.All.Matches(x => x.Posts.All(p => !p.IsDeleted))); + // check that there are no deleted posts + Assert.That(actualThreadPreviews.Data, Is.All.Matches(x => x.Posts.All(p => !p.IsDeleted))); - // check that every next thread updated earlier than the previous one (sort by LastPostCreatedAt desc) - Assert.That(actualThreadPreviews.Data, Is.Ordered - .By(nameof(ThreadPreviewModel.IsPinned)) - .Descending - .Then - .By(nameof(ThreadPreviewModel.LastBumpAt)) - .Descending); + // check that every next thread updated earlier than the previous one (sort by LastPostCreatedAt desc) + Assert.That(actualThreadPreviews.Data, Is.Ordered + .By(nameof(ThreadPreviewModel.IsPinned)) + .Descending + .Then + .By(nameof(ThreadPreviewModel.LastBumpAt)) + .Descending); - foreach (var thread in actualThreadPreviews.Data) - { - // check that posts are sorted by date ascending - Assert.That(thread.Posts, Is.Ordered.By(nameof(PostDetailsModel.CreatedAt)).Ascending); + foreach (var thread in actualThreadPreviews.Data) + { + // check that posts are sorted by date ascending + Assert.That(thread.Posts, Is.Ordered.By(nameof(PostDetailsModel.CreatedAt)).Ascending); + } } } }