diff --git a/Hikkaba.Tests.Integration/Builders/BanTestDataBuilder.cs b/Hikkaba.Tests.Integration/Builders/BanTestDataBuilder.cs new file mode 100644 index 0000000..be22e12 --- /dev/null +++ b/Hikkaba.Tests.Integration/Builders/BanTestDataBuilder.cs @@ -0,0 +1,200 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Hikkaba.Application.Contracts; +using Hikkaba.Data.Context; +using Hikkaba.Data.Entities; +using Hikkaba.Shared.Constants; +using Hikkaba.Shared.Enums; +using Hikkaba.Tests.Integration.Utils; +using Microsoft.Extensions.DependencyInjection; +using Thread = Hikkaba.Data.Entities.Thread; + +namespace Hikkaba.Tests.Integration.Builders; + +internal sealed class BanTestDataBuilder +{ + private static readonly GuidGenerator GuidGenerator = new(); + + private readonly ApplicationDbContext _dbContext; + private readonly IHashService _hashService; + private readonly TimeProvider _timeProvider; + + private ApplicationUser? _admin; + private Category? _category; + private Thread? _thread; + + public BanTestDataBuilder(IServiceScope scope) + { + _dbContext = scope.ServiceProvider.GetRequiredService(); + _hashService = scope.ServiceProvider.GetRequiredService(); + _timeProvider = scope.ServiceProvider.GetRequiredService(); + } + + public ApplicationUser Admin => _admin ?? throw new InvalidOperationException("Admin not created. Call WithDefaultAdmin() first."); + public Category Category => _category ?? throw new InvalidOperationException("Category not created. Call WithDefaultCategory() first."); + public Thread Thread => _thread ?? throw new InvalidOperationException("Thread not created. Call WithDefaultThread() first."); + + public BanTestDataBuilder WithDefaultAdmin() + { + _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); + return this; + } + + public BanTestDataBuilder WithDefaultCategory() + { + EnsureAdminExists(); + + _category = new Category + { + IsDeleted = false, + CreatedAt = _timeProvider.GetUtcNow().UtcDateTime, + ModifiedAt = null, + Alias = "b", + Name = "Random", + IsHidden = false, + DefaultBumpLimit = 500, + ShowThreadLocalUserHash = false, + ShowCountry = false, + ShowOs = false, + ShowBrowser = false, + MaxThreadCount = Defaults.MaxThreadCountInCategory, + CreatedBy = Admin, + }; + _dbContext.Categories.Add(_category); + return this; + } + + public BanTestDataBuilder WithDefaultThread() + { + EnsureCategoryExists(); + + var utcNow = _timeProvider.GetUtcNow().UtcDateTime; + _thread = new Thread + { + CreatedAt = utcNow, + LastBumpAt = utcNow, + Title = "test thread 1", + IsPinned = false, + IsClosed = false, + BumpLimit = 500, + Salt = GuidGenerator.GenerateSeededGuid(), + Category = Category, + }; + _dbContext.Threads.Add(_thread); + return this; + } + + public BanTestDataBuilder WithPost(Guid blobContainerId, string ipAddress, string userAgent, bool isOriginalPost = false) + { + EnsureThreadExists(); + + var ip = IPAddress.Parse(ipAddress); + var post = new Post + { + IsOriginalPost = isOriginalPost, + BlobContainerId = blobContainerId, + CreatedAt = _timeProvider.GetUtcNow().UtcDateTime, + IsSageEnabled = false, + MessageText = $"test post {userAgent}", + MessageHtml = $"test post {userAgent}", + UserIpAddress = ip.GetAddressBytes(), + UserAgent = userAgent, + ThreadLocalUserHash = _hashService.GetHashBytes(Thread.Salt, ip.GetAddressBytes()), + Thread = Thread, + }; + _dbContext.Posts.Add(post); + return this; + } + + public BanTestDataBuilder WithExactBan(string ipAddress, string reason) + { + EnsureAdminExists(); + + var ip = IPAddress.Parse(ipAddress); + var ipType = ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork + ? IpAddressType.IpV4 + : IpAddressType.IpV6; + + var ban = new Ban + { + CreatedAt = _timeProvider.GetUtcNow().UtcDateTime, + EndsAt = _timeProvider.GetUtcNow().UtcDateTime.AddYears(99), + IpAddressType = ipType, + BannedIpAddress = ip.GetAddressBytes(), + Reason = reason, + CreatedBy = Admin, + }; + _dbContext.Bans.Add(ban); + return this; + } + + public BanTestDataBuilder WithRangeBan( + string bannedIpAddress, + string lowerIpAddress, + string upperIpAddress, + string reason) + { + EnsureAdminExists(); + + var ip = IPAddress.Parse(bannedIpAddress); + var ipType = ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork + ? IpAddressType.IpV4 + : IpAddressType.IpV6; + + var ban = new Ban + { + CreatedAt = _timeProvider.GetUtcNow().UtcDateTime, + EndsAt = _timeProvider.GetUtcNow().UtcDateTime.AddYears(99), + IpAddressType = ipType, + BannedIpAddress = ip.GetAddressBytes(), + BannedCidrLowerIpAddress = IPAddress.Parse(lowerIpAddress).GetAddressBytes(), + BannedCidrUpperIpAddress = IPAddress.Parse(upperIpAddress).GetAddressBytes(), + Reason = reason, + CreatedBy = Admin, + }; + _dbContext.Bans.Add(ban); + return this; + } + + public async Task SaveAsync(CancellationToken cancellationToken) + { + await _dbContext.SaveChangesAsync(cancellationToken); + } + + private void EnsureAdminExists() + { + if (_admin == null) + { + throw new InvalidOperationException("Admin must be created first. Call WithDefaultAdmin()."); + } + } + + private void EnsureCategoryExists() + { + if (_category == null) + { + throw new InvalidOperationException("Category must be created first. Call WithDefaultCategory()."); + } + } + + private void EnsureThreadExists() + { + if (_thread == null) + { + throw new InvalidOperationException("Thread must be created first. Call WithDefaultThread()."); + } + } +} diff --git a/Hikkaba.Tests.Integration/Builders/PostTestDataBuilder.cs b/Hikkaba.Tests.Integration/Builders/PostTestDataBuilder.cs new file mode 100644 index 0000000..a360168 --- /dev/null +++ b/Hikkaba.Tests.Integration/Builders/PostTestDataBuilder.cs @@ -0,0 +1,153 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Hikkaba.Application.Contracts; +using Hikkaba.Data.Context; +using Hikkaba.Data.Entities; +using Hikkaba.Shared.Constants; +using Hikkaba.Tests.Integration.Utils; +using Microsoft.Extensions.DependencyInjection; +using Thread = Hikkaba.Data.Entities.Thread; + +namespace Hikkaba.Tests.Integration.Builders; + +internal sealed class PostTestDataBuilder +{ + private static readonly GuidGenerator GuidGenerator = new(); + + private readonly ApplicationDbContext _dbContext; + private readonly IHashService _hashService; + private readonly TimeProvider _timeProvider; + + private ApplicationUser? _admin; + private Category? _category; + private Thread? _thread; + + public PostTestDataBuilder(IServiceScope scope) + { + _dbContext = scope.ServiceProvider.GetRequiredService(); + _hashService = scope.ServiceProvider.GetRequiredService(); + _timeProvider = scope.ServiceProvider.GetRequiredService(); + } + + public ApplicationUser Admin => _admin ?? throw new InvalidOperationException("Admin not created. Call WithDefaultAdmin() first."); + public Category Category => _category ?? throw new InvalidOperationException("Category not created. Call WithCategory() first."); + public Thread Thread => _thread ?? throw new InvalidOperationException("Thread not created. Call WithThread() first."); + + public PostTestDataBuilder WithDefaultAdmin() + { + _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); + return this; + } + + public PostTestDataBuilder WithCategory(string alias, string name) + { + EnsureAdminExists(); + + _category = new Category + { + IsDeleted = false, + CreatedAt = _timeProvider.GetUtcNow().UtcDateTime, + ModifiedAt = null, + Alias = alias, + Name = name, + IsHidden = false, + DefaultBumpLimit = 500, + ShowThreadLocalUserHash = false, + MaxThreadCount = Defaults.MaxThreadCountInCategory, + CreatedBy = Admin, + }; + _dbContext.Categories.Add(_category); + return this; + } + + public PostTestDataBuilder WithThread(string title) + { + EnsureCategoryExists(); + + var utcNow = _timeProvider.GetUtcNow().UtcDateTime; + _thread = new Thread + { + CreatedAt = utcNow, + LastBumpAt = utcNow, + Title = title, + IsPinned = false, + IsClosed = false, + BumpLimit = 500, + Salt = GuidGenerator.GenerateSeededGuid(), + Category = Category, + }; + _dbContext.Threads.Add(_thread); + return this; + } + + public PostTestDataBuilder WithPost( + Guid blobContainerId, + string messageText, + string ipAddress, + string userAgent, + bool isOriginalPost = false, + bool isDeleted = false) + { + EnsureThreadExists(); + + var ip = IPAddress.Parse(ipAddress); + var post = new Post + { + IsOriginalPost = isOriginalPost, + IsDeleted = isDeleted, + BlobContainerId = blobContainerId, + CreatedAt = _timeProvider.GetUtcNow().UtcDateTime, + IsSageEnabled = false, + MessageText = messageText, + MessageHtml = messageText, + UserIpAddress = ip.GetAddressBytes(), + UserAgent = userAgent, + ThreadLocalUserHash = _hashService.GetHashBytes(Thread.Salt, ip.GetAddressBytes()), + Thread = Thread, + }; + _dbContext.Posts.Add(post); + return this; + } + + public async Task SaveAsync(CancellationToken cancellationToken) + { + await _dbContext.SaveChangesAsync(cancellationToken); + } + + private void EnsureAdminExists() + { + if (_admin == null) + { + throw new InvalidOperationException("Admin must be created first. Call WithDefaultAdmin()."); + } + } + + private void EnsureCategoryExists() + { + if (_category == null) + { + throw new InvalidOperationException("Category must be created first. Call WithCategory()."); + } + } + + private void EnsureThreadExists() + { + if (_thread == null) + { + throw new InvalidOperationException("Thread must be created first. Call WithThread()."); + } + } +} diff --git a/Hikkaba.Tests.Integration/Builders/ThreadTestDataBuilder.cs b/Hikkaba.Tests.Integration/Builders/ThreadTestDataBuilder.cs new file mode 100644 index 0000000..f3223c6 --- /dev/null +++ b/Hikkaba.Tests.Integration/Builders/ThreadTestDataBuilder.cs @@ -0,0 +1,403 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Blake3; +using Hikkaba.Application.Contracts; +using Hikkaba.Data.Context; +using Hikkaba.Data.Entities; +using Hikkaba.Data.Entities.Attachments; +using Hikkaba.Shared.Constants; +using Hikkaba.Tests.Integration.Utils; +using Microsoft.Extensions.DependencyInjection; +using Thread = Hikkaba.Data.Entities.Thread; + +namespace Hikkaba.Tests.Integration.Builders; + +internal sealed class ThreadTestDataBuilder +{ + private static readonly GuidGenerator GuidGenerator = new(); + + private readonly ApplicationDbContext _dbContext; + private readonly List _categories = []; + private readonly List _threads = []; + + private ApplicationUser? _admin; + + public ThreadTestDataBuilder(IServiceScope scope) + { + _dbContext = scope.ServiceProvider.GetRequiredService(); + HashService = scope.ServiceProvider.GetRequiredService(); + TimeProvider = scope.ServiceProvider.GetRequiredService(); + } + + public ApplicationUser Admin => _admin ?? throw new InvalidOperationException("Admin not created. Call WithDefaultAdmin() first."); + public IReadOnlyList Categories => _categories; + public IReadOnlyList Threads => _threads; + public IHashService HashService { get; } + + public TimeProvider TimeProvider { get; } + + public ThreadTestDataBuilder WithDefaultAdmin() + { + _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); + return this; + } + + public ThreadTestDataBuilder WithCategory(string alias, string name) + { + EnsureAdminExists(); + + var category = new Category + { + IsDeleted = false, + CreatedAt = TimeProvider.GetUtcNow().UtcDateTime, + ModifiedAt = null, + Alias = alias, + Name = name, + IsHidden = false, + DefaultBumpLimit = 500, + ShowThreadLocalUserHash = false, + ShowOs = false, + ShowBrowser = false, + ShowCountry = false, + MaxThreadCount = Defaults.MaxThreadCountInCategory, + CreatedBy = Admin, + }; + _categories.Add(category); + _dbContext.Categories.Add(category); + return this; + } + + public Category GetCategory(string alias) => + _categories.Find(c => c.Alias == alias) + ?? throw new InvalidOperationException($"Category with alias '{alias}' not found."); + + public ThreadTestDataBuilder WithThread( + string categoryAlias, + string title, + bool isPinned = false, + bool isClosed = false, + bool isDeleted = false, + int bumpLimit = 500, + DateTime? createdAt = null, + DateTime? lastBumpAt = null) + { + var category = GetCategory(categoryAlias); + var utcNow = createdAt ?? TimeProvider.GetUtcNow().UtcDateTime; + + var thread = new Thread + { + CreatedAt = utcNow, + LastBumpAt = lastBumpAt ?? utcNow, + Title = title, + IsPinned = isPinned, + IsClosed = isClosed, + IsDeleted = isDeleted, + BumpLimit = bumpLimit, + Salt = GuidGenerator.GenerateSeededGuid(), + Category = category, + }; + _threads.Add(thread); + _dbContext.Threads.Add(thread); + return this; + } + + public Thread GetThread(string title) => + _threads.Find(t => t.Title == title) + ?? throw new InvalidOperationException($"Thread with title '{title}' not found."); + + public Thread GetLastThread() => + _threads.LastOrDefault() + ?? throw new InvalidOperationException("No threads created yet."); + + public ThreadTestDataBuilder WithPost( + string threadTitle, + Guid blobContainerId, + string messageText, + string ipAddress = "127.0.0.1", + string userAgent = "Firefox", + bool isOriginalPost = false, + bool isSageEnabled = false, + bool isDeleted = false, + TimeSpan? createdAtOffset = null) + { + var thread = GetThread(threadTitle); + var ip = IPAddress.Parse(ipAddress); + + var post = new Post + { + IsOriginalPost = isOriginalPost, + BlobContainerId = blobContainerId, + CreatedAt = TimeProvider.GetUtcNow().UtcDateTime.Add(createdAtOffset ?? TimeSpan.Zero), + IsSageEnabled = isSageEnabled, + IsDeleted = isDeleted, + MessageText = messageText, + MessageHtml = messageText, + UserIpAddress = ip.GetAddressBytes(), + UserAgent = userAgent, + ThreadLocalUserHash = HashService.GetHashBytes(thread.Salt, ip.GetAddressBytes()), + Thread = thread, + }; + _dbContext.Posts.Add(post); + return this; + } + + public ThreadTestDataBuilder WithPostWithAudio( + string threadTitle, + Guid blobContainerId, + string messageText, + Guid audioBlobId, + string audioFileName, + string ipAddress = "127.0.0.1", + string userAgent = "Chrome", + TimeSpan? createdAtOffset = null) + { + var thread = GetThread(threadTitle); + var ip = IPAddress.Parse(ipAddress); + + var post = new Post + { + IsOriginalPost = false, + BlobContainerId = blobContainerId, + CreatedAt = TimeProvider.GetUtcNow().UtcDateTime.Add(createdAtOffset ?? TimeSpan.Zero), + IsSageEnabled = false, + MessageText = messageText, + MessageHtml = messageText, + UserIpAddress = ip.GetAddressBytes(), + UserAgent = userAgent, + ThreadLocalUserHash = HashService.GetHashBytes(thread.Salt, ip.GetAddressBytes()), + Thread = thread, + Audios = + [ + new Audio + { + BlobId = audioBlobId, + FileNameWithoutExtension = audioFileName, + FileExtension = "mp3", + FileSize = 3671469, + FileContentType = "audio/mpeg", + FileHash = Hasher.Hash("f61d4fbb-4cbd-4d4e-8df1-6c22c58de9cf"u8).AsSpan().ToArray(), + Title = audioFileName, + Album = "My Album", + Artist = "AI Generated Music", + DurationSeconds = 120, + }, + ], + }; + _dbContext.Posts.Add(post); + return this; + } + + public ThreadTestDataBuilder WithPostWithPicture( + string threadTitle, + Guid blobContainerId, + string messageText, + Guid pictureBlobId, + string pictureFileName, + string ipAddress = "127.0.0.1", + string userAgent = "Chrome", + TimeSpan? createdAtOffset = null) + { + var thread = GetThread(threadTitle); + var ip = IPAddress.Parse(ipAddress); + + var post = new Post + { + IsOriginalPost = false, + BlobContainerId = blobContainerId, + CreatedAt = TimeProvider.GetUtcNow().UtcDateTime.Add(createdAtOffset ?? TimeSpan.Zero), + IsSageEnabled = false, + MessageText = messageText, + MessageHtml = messageText, + UserIpAddress = ip.GetAddressBytes(), + UserAgent = userAgent, + ThreadLocalUserHash = HashService.GetHashBytes(thread.Salt, ip.GetAddressBytes()), + Thread = thread, + Pictures = + [ + new Picture + { + BlobId = pictureBlobId, + FileNameWithoutExtension = pictureFileName, + FileExtension = "jpg", + FileSize = 204316, + FileContentType = "image/jpeg", + FileHash = Hasher.Hash("6e84e6b4-5370-44c6-a319-a03a027f3905"u8).AsSpan().ToArray(), + Width = 1280, + Height = 960, + ThumbnailExtension = "jpg", + ThumbnailWidth = 128, + ThumbnailHeight = 96, + }, + ], + }; + _dbContext.Posts.Add(post); + return this; + } + + public ThreadTestDataBuilder WithThreadAndPosts( + string categoryAlias, + string title, + int postCount, + bool isPinned = false, + bool isClosed = false, + bool isDeleted = false, + int bumpLimit = 500, + bool allPostsSage = false, + bool includeDeletedPost = false, + DateTime? threadCreatedAt = null) + { + var category = GetCategory(categoryAlias); + var utcNow = threadCreatedAt ?? TimeProvider.GetUtcNow().UtcDateTime; + var salt = GuidGenerator.GenerateSeededGuid(); + + var thread = new Thread + { + CreatedAt = utcNow, + LastBumpAt = utcNow, + Title = title, + IsPinned = isPinned, + IsClosed = isClosed, + IsDeleted = isDeleted, + BumpLimit = bumpLimit, + Salt = salt, + Category = category, + }; + + var posts = Enumerable.Range(0, postCount) + .Select(i => + { + var ip = IPAddress.Parse($"127.0.0.{i % 256}").GetAddressBytes(); + return new Post + { + IsOriginalPost = i == 0, + BlobContainerId = GuidGenerator.GenerateSeededGuid(), + CreatedAt = TimeProvider.GetUtcNow().UtcDateTime.AddMinutes(i), + IsSageEnabled = allPostsSage || i % 2 == 0, + IsDeleted = false, + MessageText = $"test post {i}", + MessageHtml = $"test post {i}", + UserIpAddress = ip, + UserAgent = "Firefox", + ThreadLocalUserHash = HashService.GetHashBytes(salt, ip), + Thread = thread, + }; + }) + .ToList(); + + if (includeDeletedPost) + { + var deletedPostIp = IPAddress.Parse("127.0.0.1").GetAddressBytes(); + posts.Add(new Post + { + IsOriginalPost = false, + BlobContainerId = GuidGenerator.GenerateSeededGuid(), + CreatedAt = TimeProvider.GetUtcNow().UtcDateTime.AddYears(1), + IsSageEnabled = false, + IsDeleted = true, + MessageText = "deleted post", + MessageHtml = "deleted post", + UserIpAddress = deletedPostIp, + UserAgent = "Firefox", + ThreadLocalUserHash = HashService.GetHashBytes(salt, deletedPostIp), + Thread = thread, + }); + } + + _threads.Add(thread); + _dbContext.Threads.Add(thread); + _dbContext.Posts.AddRange(posts); + return this; + } + + public ThreadTestDataBuilder WithManyThreadsAndPosts( + string categoryAlias, + int threadCount, + int postCountPerThread, + bool includeDeletedPost = false, + Func? isPinnedSelector = null, + Func? threadCreatedAtSelector = null) + { + for (var i = 0; i < threadCount; i++) + { + var createdAt = threadCreatedAtSelector?.Invoke(i) ?? TimeProvider.GetUtcNow().UtcDateTime.AddSeconds(i); + WithThreadAndPosts( + categoryAlias, + $"test thread {i}", + postCountPerThread, + isPinned: isPinnedSelector?.Invoke(i) ?? false, + includeDeletedPost: includeDeletedPost, + threadCreatedAt: createdAt); + } + return this; + } + + public ThreadTestDataBuilder AddPostsToThread( + string threadTitle, + DateTime startingAt, + int count, + bool isSageEnabled = false, + bool isDeleted = false) + { + var thread = GetThread(threadTitle); + + for (var i = 0; i < count; i++) + { + var ip = IPAddress.Parse($"127.0.0.{i % 256}").GetAddressBytes(); + var post = new Post + { + IsOriginalPost = thread.Posts.Count == 0 && i == 0, + BlobContainerId = GuidGenerator.GenerateSeededGuid(), + CreatedAt = startingAt.AddSeconds(i), + IsSageEnabled = isSageEnabled, + IsDeleted = isDeleted, + MessageText = $"test post {i} in thread {thread.Title}", + MessageHtml = $"test post {i} in thread {thread.Title}", + UserIpAddress = ip, + UserAgent = "Firefox", + ThreadLocalUserHash = HashService.GetHashBytes(thread.Salt, ip), + Thread = thread, + }; + thread.Posts.Add(post); + } + return this; + } + + public ThreadTestDataBuilder UpdateThreadLastBumpAt(string threadTitle) + { + var thread = GetThread(threadTitle); + var lastNonSagePost = thread.Posts.Where(p => p is { IsSageEnabled: false, IsDeleted: false }).MaxBy(p => p.CreatedAt); + if (lastNonSagePost != null) + { + thread.LastBumpAt = lastNonSagePost.CreatedAt; + } + return this; + } + + public async Task SaveAsync(CancellationToken cancellationToken) + { + await _dbContext.SaveChangesAsync(cancellationToken); + } + + private void EnsureAdminExists() + { + if (_admin == null) + { + throw new InvalidOperationException("Admin must be created first. Call WithDefaultAdmin()."); + } + } +} diff --git a/Hikkaba.Tests.Integration/Models/SeedResult.cs b/Hikkaba.Tests.Integration/Models/AppScope.cs similarity index 81% rename from Hikkaba.Tests.Integration/Models/SeedResult.cs rename to Hikkaba.Tests.Integration/Models/AppScope.cs index 4e8d073..c6d2779 100644 --- a/Hikkaba.Tests.Integration/Models/SeedResult.cs +++ b/Hikkaba.Tests.Integration/Models/AppScope.cs @@ -2,7 +2,7 @@ namespace Hikkaba.Tests.Integration.Models; -internal sealed class SeedResult : ISeedResult, IAppFactorySeedResult +internal sealed class AppScope : IAppFactoryScope { public required IServiceScope Scope { get; set; } public required CustomAppFactory AppFactory { get; set; } diff --git a/Hikkaba.Tests.Integration/Models/IAppFactoryScope.cs b/Hikkaba.Tests.Integration/Models/IAppFactoryScope.cs new file mode 100644 index 0000000..cef16f5 --- /dev/null +++ b/Hikkaba.Tests.Integration/Models/IAppFactoryScope.cs @@ -0,0 +1,6 @@ +namespace Hikkaba.Tests.Integration.Models; + +internal interface IAppFactoryScope : IAppScope +{ + CustomAppFactory AppFactory { get; set; } +} diff --git a/Hikkaba.Tests.Integration/Models/IAppFactorySeedResult.cs b/Hikkaba.Tests.Integration/Models/IAppFactorySeedResult.cs deleted file mode 100644 index 39d5429..0000000 --- a/Hikkaba.Tests.Integration/Models/IAppFactorySeedResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -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/IAppScope.cs similarity index 78% rename from Hikkaba.Tests.Integration/Models/ISeedResult.cs rename to Hikkaba.Tests.Integration/Models/IAppScope.cs index 542951b..be27c33 100644 --- a/Hikkaba.Tests.Integration/Models/ISeedResult.cs +++ b/Hikkaba.Tests.Integration/Models/IAppScope.cs @@ -3,7 +3,7 @@ namespace Hikkaba.Tests.Integration.Models; -internal interface ISeedResult : IDisposable +internal interface IAppScope : IDisposable { public IServiceScope Scope { get; set; } } diff --git a/Hikkaba.Tests.Integration/Tests/Repositories/BanRepositoryTests.cs b/Hikkaba.Tests.Integration/Tests/Repositories/BanRepositoryTests.cs index 01979c9..6462a4d 100644 --- a/Hikkaba.Tests.Integration/Tests/Repositories/BanRepositoryTests.cs +++ b/Hikkaba.Tests.Integration/Tests/Repositories/BanRepositoryTests.cs @@ -3,15 +3,13 @@ using System.Net; using System.Threading; using System.Threading.Tasks; -using Hikkaba.Application.Contracts; using Hikkaba.Data.Context; using Hikkaba.Data.Entities; using Hikkaba.Infrastructure.Models.Ban; using Hikkaba.Infrastructure.Repositories.Contracts; using Hikkaba.Paging.Enums; using Hikkaba.Paging.Models; -using Hikkaba.Shared.Constants; -using Hikkaba.Shared.Enums; +using Hikkaba.Tests.Integration.Builders; using Hikkaba.Tests.Integration.Constants; using Hikkaba.Tests.Integration.Extensions; using Hikkaba.Tests.Integration.Models; @@ -20,7 +18,6 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Thread = Hikkaba.Data.Entities.Thread; namespace Hikkaba.Tests.Integration.Tests.Repositories; @@ -28,8 +25,6 @@ namespace Hikkaba.Tests.Integration.Tests.Repositories; [Parallelizable(scope: ParallelScope.Fixtures)] internal sealed class BanRepositoryTests { - private static readonly GuidGenerator GuidGenerator = new(); - private RespawnableContextManager? _contextManager; [OneTimeSetUp] @@ -45,7 +40,7 @@ public async Task OneTimeTearDownAsync() } [MustDisposeResource] - private async Task Seed(CancellationToken cancellationToken) + private async Task CreateSeedResultAsync(CancellationToken cancellationToken) { var connectionString = await _contextManager!.CreateRespawnedDbConnectionStringAsync(); var customAppFactory = new CustomAppFactory(connectionString); @@ -58,13 +53,39 @@ private async Task Seed(CancellationToken cancellationToken) await dbContext.Database.MigrateAsync(cancellationToken); } - return new SeedResult + return new AppScope { Scope = scope, AppFactory = customAppFactory, }; } + private static async Task SeedExactBansDataAsync(IServiceScope scope, CancellationToken cancellationToken) + { + await new BanTestDataBuilder(scope) + .WithDefaultAdmin() + .WithDefaultCategory() + .WithDefaultThread() + .WithPost(new Guid("05E219F7-35F2-495B-A0D3-D7EF7018C674"), "176.213.241.52", "Firefox", isOriginalPost: true) + .WithPost(new Guid("EADF6C08-1C14-432E-A9EB-0DDF67D55FC7"), "b550:f112:2801:51d4:fdaf:21d8:6bbc:aaba", "Chrome") + .WithExactBan("176.213.241.52", "ban reason 1") + .WithExactBan("b550:f112:2801:51d4:fdaf:21d8:6bbc:aaba", "ban reason 2") + .SaveAsync(cancellationToken); + } + + private static async Task SeedRangeBansDataAsync(IServiceScope scope, CancellationToken cancellationToken) + { + await new BanTestDataBuilder(scope) + .WithDefaultAdmin() + .WithDefaultCategory() + .WithDefaultThread() + .WithPost(new Guid("64596344-BC44-489A-9D6E-1AA2BB5A27BF"), "176.213.224.37", "Firefox", isOriginalPost: true) + .WithPost(new Guid("9BC6094D-DD51-4C59-8EAB-444446DEEF62"), "2001:4860:0000:0000:0000:0000:ffff:0", "Chrome") + .WithRangeBan("176.213.224.40", "176.213.224.1", "176.213.224.254", "ban reason 1") + .WithRangeBan("2001:4860:0000:0000:ffff:0000:0000:0", "2001:4860:0000:0000:0000:0000:0000:0", "2001:4860:ffff:ffff:ffff:ffff:ffff:ffff", "ban reason 2") + .SaveAsync(cancellationToken); + } + [CancelAfter(TestDefaults.TestTimeout)] [TestCase("176.213.241.52", true)] [TestCase("b550:f112:2801:51d4:fdaf:21d8:6bbc:aaba", true)] @@ -76,107 +97,8 @@ public async Task ListBansPaginatedAsync_WhenSearchExact_ReturnsExpectedResult( CancellationToken cancellationToken) { // Arrange - 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 - { - 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); - - var category = new Category - { - IsDeleted = false, - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - ModifiedAt = null, - Alias = "b", - Name = "Random Foo", - IsHidden = false, - DefaultBumpLimit = 500, - ShowThreadLocalUserHash = false, - ShowCountry = false, - ShowOs = false, - ShowBrowser = false, - MaxThreadCount = Defaults.MaxThreadCountInCategory, - CreatedBy = admin, - }; - dbContext.Categories.Add(category); - - var utcNow = timeProvider.GetUtcNow().UtcDateTime; - var thread = new Thread - { - CreatedAt = utcNow, - LastBumpAt = utcNow, - Title = "test thread 1 Buzz", - IsPinned = false, - IsClosed = false, - BumpLimit = 500, - Salt = GuidGenerator.GenerateSeededGuid(), - Category = category, - }; - dbContext.Threads.Add(thread); - - var post1 = new Post - { - IsOriginalPost = true, - BlobContainerId = new Guid("05E219F7-35F2-495B-A0D3-D7EF7018C674"), - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - IsSageEnabled = false, - MessageText = "test post 1 abc", - MessageHtml = "test post 1 abc", - UserIpAddress = IPAddress.Parse("176.213.241.52").GetAddressBytes(), - UserAgent = "Firefox", - ThreadLocalUserHash = hashService.GetHashBytes(thread.Salt, IPAddress.Parse("176.213.241.52").GetAddressBytes()), - Thread = thread, - }; - var post2 = new Post - { - IsOriginalPost = false, - BlobContainerId = new Guid("EADF6C08-1C14-432E-A9EB-0DDF67D55FC7"), - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - IsSageEnabled = false, - MessageText = "test post 2 def", - MessageHtml = "test post 2 def", - UserIpAddress = IPAddress.Parse("b550:f112:2801:51d4:fdaf:21d8:6bbc:aaba").GetAddressBytes(), - UserAgent = "Chrome", - ThreadLocalUserHash = hashService.GetHashBytes(thread.Salt, IPAddress.Parse("b550:f112:2801:51d4:fdaf:21d8:6bbc:aaba").GetAddressBytes()), - Thread = thread, - }; - dbContext.Posts.AddRange(post1, post2); - - var ban1 = new Ban - { - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - EndsAt = timeProvider.GetUtcNow().UtcDateTime.AddYears(99), - IpAddressType = IpAddressType.IpV4, - BannedIpAddress = IPAddress.Parse("176.213.241.52").GetAddressBytes(), - Reason = "ban reason 1", - CreatedBy = admin, - }; - var ban2 = new Ban - { - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - EndsAt = timeProvider.GetUtcNow().UtcDateTime.AddYears(99), - IpAddressType = IpAddressType.IpV6, - BannedIpAddress = IPAddress.Parse("b550:f112:2801:51d4:fdaf:21d8:6bbc:aaba").GetAddressBytes(), - Reason = "ban reason 2", - CreatedBy = admin, - }; - dbContext.Bans.AddRange(ban1, ban2); - - await dbContext.SaveChangesAsync(cancellationToken); + using var seedResult = await CreateSeedResultAsync(cancellationToken); + await SeedExactBansDataAsync(seedResult.Scope, cancellationToken); var repository = seedResult.Scope.ServiceProvider.GetRequiredService(); @@ -205,110 +127,8 @@ public async Task ListBansPaginatedAsync_WhenSearchInRange_ReturnsExpectedResult CancellationToken cancellationToken) { // Arrange - 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 - { - 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); - - var category = new Category - { - IsDeleted = false, - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - ModifiedAt = null, - Alias = "b", - Name = "Random Foo", - IsHidden = false, - DefaultBumpLimit = 500, - ShowThreadLocalUserHash = false, - ShowCountry = false, - ShowOs = false, - ShowBrowser = false, - MaxThreadCount = Defaults.MaxThreadCountInCategory, - CreatedBy = admin, - }; - dbContext.Categories.Add(category); - - var utcNow = timeProvider.GetUtcNow().UtcDateTime; - var thread = new Thread - { - CreatedAt = utcNow, - LastBumpAt = utcNow, - Title = "test thread 1 Buzz", - IsPinned = false, - IsClosed = false, - BumpLimit = 500, - Salt = GuidGenerator.GenerateSeededGuid(), - Category = category, - }; - dbContext.Threads.Add(thread); - - var post1 = new Post - { - IsOriginalPost = true, - BlobContainerId = new Guid("64596344-BC44-489A-9D6E-1AA2BB5A27BF"), - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - IsSageEnabled = false, - MessageText = "test post 1 abc", - MessageHtml = "test post 1 abc", - UserIpAddress = IPAddress.Parse("176.213.224.37").GetAddressBytes(), - UserAgent = "Firefox", - ThreadLocalUserHash = hashService.GetHashBytes(thread.Salt, IPAddress.Parse("176.213.224.37").GetAddressBytes()), - Thread = thread, - }; - var post2 = new Post - { - IsOriginalPost = false, - BlobContainerId = new Guid("9BC6094D-DD51-4C59-8EAB-444446DEEF62"), - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - IsSageEnabled = false, - MessageText = "test post 2 def", - MessageHtml = "test post 2 def", - UserIpAddress = IPAddress.Parse("2001:4860:0000:0000:0000:0000:ffff:0").GetAddressBytes(), - UserAgent = "Chrome", - ThreadLocalUserHash = hashService.GetHashBytes(thread.Salt, IPAddress.Parse("2001:4860:0000:0000:0000:0000:ffff:0").GetAddressBytes()), - Thread = thread, - }; - dbContext.Posts.AddRange(post1, post2); - - var ban1 = new Ban - { - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - EndsAt = timeProvider.GetUtcNow().UtcDateTime.AddYears(99), - IpAddressType = IpAddressType.IpV4, - BannedIpAddress = IPAddress.Parse("176.213.224.40").GetAddressBytes(), - BannedCidrLowerIpAddress = IPAddress.Parse("176.213.224.1").GetAddressBytes(), - BannedCidrUpperIpAddress = IPAddress.Parse("176.213.224.254").GetAddressBytes(), - Reason = "ban reason 1", - CreatedBy = admin, - }; - var ban2 = new Ban - { - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - EndsAt = timeProvider.GetUtcNow().UtcDateTime.AddYears(99), - IpAddressType = IpAddressType.IpV6, - BannedIpAddress = IPAddress.Parse("2001:4860:0000:0000:ffff:0000:0000:0").GetAddressBytes(), - BannedCidrLowerIpAddress = IPAddress.Parse("2001:4860:0000:0000:0000:0000:0000:0").GetAddressBytes(), - BannedCidrUpperIpAddress = IPAddress.Parse("2001:4860:ffff:ffff:ffff:ffff:ffff:ffff").GetAddressBytes(), - Reason = "ban reason 2", - CreatedBy = admin, - }; - dbContext.Bans.AddRange(ban1, ban2); - - await dbContext.SaveChangesAsync(cancellationToken); + using var seedResult = await CreateSeedResultAsync(cancellationToken); + await SeedRangeBansDataAsync(seedResult.Scope, cancellationToken); var repository = seedResult.Scope.ServiceProvider.GetRequiredService(); diff --git a/Hikkaba.Tests.Integration/Tests/Repositories/PostRepositoryTests.cs b/Hikkaba.Tests.Integration/Tests/Repositories/PostRepositoryTests.cs index c80e28f..8e7948e 100644 --- a/Hikkaba.Tests.Integration/Tests/Repositories/PostRepositoryTests.cs +++ b/Hikkaba.Tests.Integration/Tests/Repositories/PostRepositoryTests.cs @@ -1,17 +1,14 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; -using Hikkaba.Application.Contracts; using Hikkaba.Data.Context; using Hikkaba.Data.Entities; using Hikkaba.Infrastructure.Models.Post; using Hikkaba.Infrastructure.Repositories.Contracts; using Hikkaba.Paging.Enums; using Hikkaba.Paging.Models; -using Hikkaba.Shared.Constants; +using Hikkaba.Tests.Integration.Builders; using Hikkaba.Tests.Integration.Constants; using Hikkaba.Tests.Integration.Extensions; using Hikkaba.Tests.Integration.Models; @@ -21,7 +18,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Thread = Hikkaba.Data.Entities.Thread; namespace Hikkaba.Tests.Integration.Tests.Repositories; @@ -29,7 +25,6 @@ namespace Hikkaba.Tests.Integration.Tests.Repositories; [Parallelizable(scope: ParallelScope.Fixtures)] internal sealed class PostRepositoryTests { - private static readonly GuidGenerator GuidGenerator = new(); private RespawnableContextManager? _contextManager; [OneTimeSetUp] @@ -45,7 +40,7 @@ public async Task OneTimeTearDownAsync() } [MustDisposeResource] - private async Task Seed(CancellationToken cancellationToken) + private async Task CreateSeedResultAsync(CancellationToken cancellationToken) { var connectionString = await _contextManager!.CreateRespawnedDbConnectionStringAsync(); var customAppFactory = new CustomAppFactory(connectionString); @@ -58,13 +53,25 @@ private async Task Seed(CancellationToken cancellationToken) await dbContext.Database.MigrateAsync(cancellationToken); } - return new SeedResult + return new AppScope { Scope = scope, AppFactory = customAppFactory, }; } + private static async Task SeedSearchPostsDataAsync(IServiceScope scope, CancellationToken cancellationToken) + { + await new PostTestDataBuilder(scope) + .WithDefaultAdmin() + .WithCategory("b", "Random CategorySearchTerm") + .WithThread("BoardThreadPostSearchTerm thread 1 ThreadAndPostSearchTerm") + .WithPost(new Guid("243D7DB4-4EE8-4285-8888-E7185A7CB1B2"), "BoardThreadPostSearchTerm post 1 Post1SearchTerm", "127.0.0.1", "Firefox", isOriginalPost: true) + .WithPost(new Guid("D9AED982-37D6-4C5C-B235-E1AADC342236"), "BoardThreadPostSearchTerm post 2 Post2SearchTerm", "127.0.0.1", "Chrome") + .WithPost(new Guid("C8393E45-20AE-4214-A1EF-5F6AE0D93477"), "BoardThreadPostSearchTerm Post1SearchTerm Post2SearchTerm BoardSearchTerm CategorySearchTerm ThreadAndPostSearchTerm", "127.0.0.1", "Chrome", isDeleted: true) + .SaveAsync(cancellationToken); + } + [CancelAfter(TestDefaults.TestTimeout)] [TestCase("BoardSearchTerm", 0)] [TestCase("CategorySearchTerm", 0)] @@ -78,100 +85,11 @@ public async Task SearchPostsPaginatedAsync_WhenSearchQueryIsProvided_ReturnsExp CancellationToken cancellationToken) { // Arrange - using var seedResult = await Seed(cancellationToken); + using var seedResult = await CreateSeedResultAsync(cancellationToken); + await SeedSearchPostsDataAsync(seedResult.Scope, 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 - { - 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); - - var category = new Category - { - IsDeleted = false, - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - ModifiedAt = null, - Alias = "b", - Name = "Random CategorySearchTerm", - IsHidden = false, - DefaultBumpLimit = 500, - ShowThreadLocalUserHash = false, - MaxThreadCount = Defaults.MaxThreadCountInCategory, - CreatedBy = admin, - }; - dbContext.Categories.Add(category); - - var utcNow = timeProvider.GetUtcNow().UtcDateTime; - var thread = new Thread - { - CreatedAt = utcNow, - LastBumpAt = utcNow, - Title = "BoardThreadPostSearchTerm thread 1 ThreadAndPostSearchTerm", - IsPinned = false, - IsClosed = false, - BumpLimit = 500, - Salt = GuidGenerator.GenerateSeededGuid(), - Category = category, - }; - dbContext.Threads.Add(thread); - - var post1 = new Post - { - IsOriginalPost = true, - BlobContainerId = new Guid("243D7DB4-4EE8-4285-8888-E7185A7CB1B2"), - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - IsSageEnabled = false, - MessageText = "BoardThreadPostSearchTerm post 1 Post1SearchTerm", - MessageHtml = "BoardThreadPostSearchTerm post 1 Post1SearchTerm", - UserIpAddress = IPAddress.Parse("127.0.0.1").GetAddressBytes(), - UserAgent = "Firefox", - ThreadLocalUserHash = hashService.GetHashBytes(thread.Salt, IPAddress.Parse("127.0.0.1").GetAddressBytes()), - Thread = thread, - }; - var post2 = new Post - { - IsOriginalPost = false, - BlobContainerId = new Guid("D9AED982-37D6-4C5C-B235-E1AADC342236"), - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - IsSageEnabled = false, - MessageText = "BoardThreadPostSearchTerm post 2 Post2SearchTerm", - MessageHtml = "BoardThreadPostSearchTerm post 2 Post2SearchTerm", - UserIpAddress = IPAddress.Parse("127.0.0.1").GetAddressBytes(), - UserAgent = "Chrome", - ThreadLocalUserHash = hashService.GetHashBytes(thread.Salt, IPAddress.Parse("127.0.0.1").GetAddressBytes()), - Thread = thread, - }; - var post3 = new Post - { - IsOriginalPost = false, - BlobContainerId = new Guid("C8393E45-20AE-4214-A1EF-5F6AE0D93477"), - CreatedAt = timeProvider.GetUtcNow().UtcDateTime, - IsDeleted = true, - IsSageEnabled = false, - MessageText = "BoardThreadPostSearchTerm Post1SearchTerm Post2SearchTerm BoardSearchTerm CategorySearchTerm ThreadAndPostSearchTerm", - MessageHtml = "BoardThreadPostSearchTerm Post1SearchTerm Post2SearchTerm BoardSearchTerm CategorySearchTerm ThreadAndPostSearchTerm", - UserIpAddress = IPAddress.Parse("127.0.0.1").GetAddressBytes(), - UserAgent = "Chrome", - ThreadLocalUserHash = hashService.GetHashBytes(thread.Salt, IPAddress.Parse("127.0.0.1").GetAddressBytes()), - Thread = thread, - }; - IReadOnlyList allPosts = [post1, post2, post3]; - dbContext.Posts.AddRange(allPosts); - - await dbContext.SaveChangesAsync(cancellationToken); - await DbUtils.WaitForFulltextIndexAsync(logger, dbContext, ["Posts", "Threads"], cancellationToken: cancellationToken); var repository = seedResult.Scope.ServiceProvider.GetRequiredService(); diff --git a/Hikkaba.Tests.Integration/Tests/Repositories/ThreadRepositoryTests.cs b/Hikkaba.Tests.Integration/Tests/Repositories/ThreadRepositoryTests.cs index e216b61..f5955f2 100644 --- a/Hikkaba.Tests.Integration/Tests/Repositories/ThreadRepositoryTests.cs +++ b/Hikkaba.Tests.Integration/Tests/Repositories/ThreadRepositoryTests.cs @@ -1,20 +1,16 @@ using System; -using System.Collections.Generic; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; -using Blake3; using Hikkaba.Application.Contracts; using Hikkaba.Data.Context; -using Hikkaba.Data.Entities; -using Hikkaba.Data.Entities.Attachments; using Hikkaba.Infrastructure.Models.Post; using Hikkaba.Infrastructure.Models.Thread; using Hikkaba.Infrastructure.Repositories.Contracts; using Hikkaba.Paging.Enums; using Hikkaba.Paging.Models; using Hikkaba.Shared.Constants; +using Hikkaba.Tests.Integration.Builders; using Hikkaba.Tests.Integration.Constants; using Hikkaba.Tests.Integration.Extensions; using Hikkaba.Tests.Integration.Models; @@ -23,7 +19,6 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Thread = Hikkaba.Data.Entities.Thread; namespace Hikkaba.Tests.Integration.Tests.Repositories; @@ -31,8 +26,6 @@ namespace Hikkaba.Tests.Integration.Tests.Repositories; [Parallelizable(scope: ParallelScope.Fixtures)] internal sealed class ThreadRepositoryTests { - private static readonly GuidGenerator GuidGenerator = new(); - private RespawnableContextManager? _contextManager; [OneTimeSetUp] @@ -48,7 +41,7 @@ public async Task OneTimeTearDownAsync() } [MustDisposeResource] - private async Task Seed(CancellationToken cancellationToken) + private async Task CreateAppScopeAsync(CancellationToken cancellationToken) { var connectionString = await _contextManager!.CreateRespawnedDbConnectionStringAsync(); var customAppFactory = new CustomAppFactory(connectionString); @@ -61,190 +54,199 @@ private async Task Seed(CancellationToken cancellationToken) await dbContext.Database.MigrateAsync(cancellationToken); } - return new SeedResult + return new AppScope { Scope = scope, AppFactory = customAppFactory, }; } - [CancelAfter(TestDefaults.TestTimeout)] - [Test] - public async Task ListThreadPreviewsPaginatedAsync_WhenOnePageExists_ReturnsCorrectResult( + #region Seed Methods + + private static async Task<(ThreadTestDataBuilder Builder, int PostCount)> SeedOnePageDataAsync( + IServiceScope scope, CancellationToken cancellationToken) { - // Arrange - 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 builder = new ThreadTestDataBuilder(scope) + .WithDefaultAdmin() + .WithCategory("b", "Random") + .WithThread("b", "test thread 1") + .WithPost("test thread 1", new Guid("545917CA-374F-4C34-80B9-7D8DF0842D72"), "test post 0", isOriginalPost: true, createdAtOffset: TimeSpan.FromSeconds(1)) + .WithPostWithAudio("test thread 1", new Guid("502FACD5-C207-4684-960B-274949E6D043"), "test post 1", new Guid("6D3CD116-6336-47BC-BBE7-5DB289AC6C51"), "Extended electric guitar solo", createdAtOffset: TimeSpan.FromSeconds(2)) + .WithPostWithPicture("test thread 1", new Guid("91F9A825-FFC0-45FA-B8CF-EA0435F414BC"), "test post 2", new Guid("668B2737-0540-4DDD-A23E-58FA031A933F"), "photo_2024-10-31_16-20-39", createdAtOffset: TimeSpan.FromSeconds(3)) + .WithPost("test thread 1", new Guid("BD852887-CBE3-4BAB-9FAC-F501EC3DA439"), "test post 3", createdAtOffset: TimeSpan.FromSeconds(4)) + .WithPost("test thread 1", new Guid("2FA199CC-CD14-402D-8209-0A1B8353E463"), "test post 4", createdAtOffset: TimeSpan.FromSeconds(5)) + .WithPost("test thread 1", new Guid("1F657883-6C50-48FE-982C-5E1B552918D3"), "test post 5", createdAtOffset: TimeSpan.FromSeconds(6)); + + await builder.SaveAsync(cancellationToken); + return (builder, 6); + } - // 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); + private static async Task SeedManyPagesDataAsync( + IServiceScope scope, + int totalThreadCount, + int totalPostCountPerThread, + CancellationToken cancellationToken) + { + var builder = new ThreadTestDataBuilder(scope) + .WithDefaultAdmin() + .WithCategory("b", "Random") + .WithCategory("a", "Anime"); + + // Deleted thread (should be excluded) + builder.WithThreadAndPosts("b", "deleted thread", 1, isPinned: true, isClosed: true, isDeleted: true); + + // Thread in another category (should be excluded) + builder.WithThreadAndPosts("a", "another category thread", 1); + + // Main threads + builder.WithManyThreadsAndPosts( + "b", + totalThreadCount, + totalPostCountPerThread, + includeDeletedPost: true, + threadCreatedAtSelector: i => builder.TimeProvider.GetUtcNow().UtcDateTime.AddSeconds(i)); + + await builder.SaveAsync(cancellationToken); + } - var category = 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, - }; - dbContext.Categories.Add(category); + private static async Task SeedPinnedThreadDataAsync( + IServiceScope scope, + int totalThreadCount, + int totalPostCountPerThread, + CancellationToken cancellationToken) + { + var builder = new ThreadTestDataBuilder(scope) + .WithDefaultAdmin() + .WithCategory("b", "Random") + .WithCategory("a", "Anime"); + + // Deleted thread + builder.WithThreadAndPosts("b", "deleted thread", 1, isPinned: true, isClosed: true, isDeleted: true); + + // Thread in another category + builder.WithThreadAndPosts("a", "another category thread", 1); + + // Main threads with one pinned + builder.WithManyThreadsAndPosts( + "b", + totalThreadCount, + totalPostCountPerThread, + includeDeletedPost: true, + isPinnedSelector: i => i == 3); + + await builder.SaveAsync(cancellationToken); + } - var utcNow = timeProvider.GetUtcNow().UtcDateTime; - var thread = new Thread - { - CreatedAt = utcNow, - LastBumpAt = utcNow, - Title = "test thread 1 Buzz", - IsPinned = false, - IsClosed = false, - BumpLimit = 500, - Salt = GuidGenerator.GenerateSeededGuid(), - Category = category, - }; - dbContext.Threads.Add(thread); + private static async Task SeedSagePostDataAsync( + IServiceScope scope, + int totalThreadCount, + int totalPostCountPerThread, + CancellationToken cancellationToken) + { + var builder = new ThreadTestDataBuilder(scope) + .WithDefaultAdmin() + .WithCategory("b", "Random") + .WithCategory("a", "Anime"); - var userIp = IPAddress.Parse("127.0.0.1").GetAddressBytes(); - var post0 = new Post - { - IsOriginalPost = true, - BlobContainerId = new Guid("545917CA-374F-4C34-80B9-7D8DF0842D72"), - CreatedAt = timeProvider.GetUtcNow().UtcDateTime.AddSeconds(1), - IsSageEnabled = false, - MessageText = "test post 0", - MessageHtml = "test post 0", - UserIpAddress = userIp, - UserAgent = "Firefox", - ThreadLocalUserHash = hashService.GetHashBytes(thread.Salt, userIp), - Thread = thread, - }; - var post1 = new Post - { - IsOriginalPost = false, - BlobContainerId = new Guid("502FACD5-C207-4684-960B-274949E6D043"), - CreatedAt = timeProvider.GetUtcNow().UtcDateTime.AddSeconds(2), - IsSageEnabled = false, - MessageText = "test post 1", - MessageHtml = "test post 1", - UserIpAddress = userIp, - UserAgent = "Chrome", - ThreadLocalUserHash = hashService.GetHashBytes(thread.Salt, userIp), - Thread = thread, - Audios = new List