diff --git a/.editorconfig b/.editorconfig index 5bad9d5..32a0530 100644 --- a/.editorconfig +++ b/.editorconfig @@ -81,7 +81,6 @@ dotnet_remove_unnecessary_suppression_exclusions = none # analyzer settings dotnet_diagnostic.IDE0058.severity = none -dotnet_diagnostic.IDE0053.severity = when_on_single_line:suggestion dotnet_diagnostic.IDE0100.severity = none dotnet_diagnostic.CA1515.severity = none dotnet_diagnostic.CA1848.severity = none @@ -95,7 +94,7 @@ csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_lambdas = true:suggestion +csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion csharp_style_expression_bodied_local_functions = false:silent csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_operators = false:silent diff --git a/tests/FiscalOS.API.Tests/Infra/HttpRequestBuilder.cs b/tests/FiscalOS.API.Tests/Infra/HttpRequestBuilder.cs index e88b4dc..8980f6d 100644 --- a/tests/FiscalOS.API.Tests/Infra/HttpRequestBuilder.cs +++ b/tests/FiscalOS.API.Tests/Infra/HttpRequestBuilder.cs @@ -13,7 +13,10 @@ internal sealed class HttpRequestBuilder { } - public static HttpRequestBuilder New() => new(); + public static HttpRequestBuilder New() + { + return new(); + } public HttpRequestBuilder WithMethod(HttpMethod method) { diff --git a/tests/FiscalOS.API.Tests/Infra/TestApi.cs b/tests/FiscalOS.API.Tests/Infra/TestApi.cs index 096fb52..8a80377 100644 --- a/tests/FiscalOS.API.Tests/Infra/TestApi.cs +++ b/tests/FiscalOS.API.Tests/Infra/TestApi.cs @@ -2,6 +2,8 @@ namespace FiscalOS.API.Tests.Infra; public class TestApi : WebApplicationFactory { + private readonly List> _additionalConfigs = []; + protected override void ConfigureWebHost(IWebHostBuilder builder) { base.ConfigureWebHost(builder); @@ -25,5 +27,71 @@ public class TestApi : WebApplicationFactory c.AddSingleton(TestKeyRing.From); }); + + foreach (var config in _additionalConfigs) + { + config(builder); + } + } + + public TestApi WithAdditionalConfig(Action configuration) + { + var newApi = new TestApi(); + + newApi._additionalConfigs.AddRange(_additionalConfigs); + newApi._additionalConfigs.Add(configuration); + + return newApi; + } + + public async Task ExecuteAsync(Func action, CancellationToken ct) + { + await using var scope = Services.CreateAsyncScope(); + var context = scope.ServiceProvider.GetRequiredService(); + await action(context, ct); + } + + public async Task ExecuteAsync(Func action, CancellationToken ct) + { + await using var scope = Services.CreateAsyncScope(); + var context = scope.ServiceProvider.GetRequiredService(); + await action(context, ct, scope.ServiceProvider); + } + + public async Task ExecuteAsync(Func> action, CancellationToken ct) + { + await using var scope = Services.CreateAsyncScope(); + var context = scope.ServiceProvider.GetRequiredService(); + return await action(context, ct); + } + + public async Task ExecuteAsync(Func> action, CancellationToken ct) + { + await using var scope = Services.CreateAsyncScope(); + var context = scope.ServiceProvider.GetRequiredService(); + return await action(context, ct, scope.ServiceProvider); + } + + public async Task EnsureDbCreatedAsync() + { + await ExecuteAsync(static async (context, ct) => + { + await context.Database.EnsureCreatedAsync(ct); + }, TestContext.Current.CancellationToken); + } + + public async Task EnsureDbDeletedAsync() + { + await ExecuteAsync(static async (context, ct) => + { + await context.Database.EnsureDeletedAsync(ct); + }, TestContext.Current.CancellationToken); + } + + public override async ValueTask DisposeAsync() + { + await EnsureDbDeletedAsync(); + await base.DisposeAsync(); + GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/tests/FiscalOS.API.Tests/Integration/Accounts/AddTests.cs b/tests/FiscalOS.API.Tests/Integration/Accounts/AddTests.cs index 2c91552..b9297db 100644 --- a/tests/FiscalOS.API.Tests/Integration/Accounts/AddTests.cs +++ b/tests/FiscalOS.API.Tests/Integration/Accounts/AddTests.cs @@ -1,3 +1,6 @@ +using FiscalOS.Core.Queuing; +using FiscalOS.Infra.Transactions.Plaid; + using Account = FiscalOS.Core.Accounts.Account; using Institution = FiscalOS.Core.Accounts.Institution; @@ -31,7 +34,6 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) ["PlaidInstitutionId"] = ["The PlaidInstitutionId field is required."], ["PlaidAccountId"] = ["The PlaidAccountId field is required."], ["PlaidAccountName"] = ["The PlaidAccountName field is required."], - ["AccountCurrencyCode"] = ["The AccountCurrencyCode field is required."], }); } @@ -124,7 +126,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Add_WhenCalledWithPlaidInstitutionIdThatHasNotBeenAdded_ItShouldReturn400WithProblemDetails() { - var user = await ExecuteAsync(static async (context, ct, sp) => + var user = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); @@ -161,7 +163,7 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Add_WhenCalledWithPlaidAccountIdThatHasAlreadyBeenAdded_ItShouldReturn409WithProblemDetails() { - var (user, institution, account) = await ExecuteAsync(static async (context, ct, sp) => + var (user, institution, account) = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); @@ -212,7 +214,18 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Add_WhenCalledWithNewAccount_ItShouldReturn200() { - var (user, institution) = await ExecuteAsync(async (context, ct, sp) => + var mockQueue = new Mock>(); + + await using var testApi = Api + .WithAdditionalConfig(whb => + { + whb.ConfigureTestServices(s => + { + s.AddSingleton(mockQueue.Object); + }); + }); + + var (user, institution) = await testApi.ExecuteAsync(async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); @@ -256,11 +269,13 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) }) .Build(); - var response = await Client.SendAsync(request, TestContext.Current.CancellationToken); + var client = testApi.CreateClient(); + + var response = await client.SendAsync(request, TestContext.Current.CancellationToken); response.StatusCode.Should().Be(HttpStatusCode.OK); - var updatedUser = await ExecuteAsync( + var updatedUser = await testApi.ExecuteAsync( async (context, ct) => await context.Set() .Include(u => u.Accounts) .ThenInclude(a => a.Metadata) @@ -279,12 +294,9 @@ public class AddTests(TestApi testApi) : IntegrationTest(testApi) ((PlaidAccountMetadata)a.Metadata).PlaidName == newAccountName ); - updatedUser.Accounts.First() - .Balances.Should().ContainSingle( - b => b.AccountId == updatedUser.Accounts.First().Id && - b.Current == expectedBalance && - b.Available == expectedBalance && - b.CurrencyCode == expectedCurrencyCode - ); + mockQueue.Verify(m => m.EnqueueAsync( + It.IsAny(), + It.IsAny() + ), Times.Once()); } } \ No newline at end of file diff --git a/tests/FiscalOS.API.Tests/Integration/Auth/LoginTests.cs b/tests/FiscalOS.API.Tests/Integration/Auth/LoginTests.cs index d0432b6..a0e4a8a 100644 --- a/tests/FiscalOS.API.Tests/Integration/Auth/LoginTests.cs +++ b/tests/FiscalOS.API.Tests/Integration/Auth/LoginTests.cs @@ -42,7 +42,7 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Login_WhenUserExistsButPasswordIsIncorrect_ItShouldReturn401WithProblemDetails() { - await ExecuteAsync(static async (context, ct, sp) => + await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); @@ -70,7 +70,7 @@ public class LoginTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Login_WhenUserExistsAndPasswordIsCorrect_ItShouldReturn200WithJwtTokenAndSetRefreshCookie() { - await ExecuteAsync(static async (context, ct, sp) => + await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); diff --git a/tests/FiscalOS.API.Tests/Integration/Auth/RefreshTests.cs b/tests/FiscalOS.API.Tests/Integration/Auth/RefreshTests.cs index be66c66..6921ca2 100644 --- a/tests/FiscalOS.API.Tests/Integration/Auth/RefreshTests.cs +++ b/tests/FiscalOS.API.Tests/Integration/Auth/RefreshTests.cs @@ -29,7 +29,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Refresh_WhenCalledWithTokenBelongingToDifferentUser_ItShouldReturn403WithProblemDetails() { - var (users, refreshToken) = await ExecuteAsync(static async (context, ct, sp) => + var (users, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var tokenGenerator = sp.GetRequiredService(); @@ -61,7 +61,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi) await response.Should().BeProblemDetails(HttpStatusCode.Forbidden); - var unrevokedTokensCountForUser2 = await ExecuteAsync( + var unrevokedTokensCountForUser2 = await Api.ExecuteAsync( async (context, ct) => await context.Set() .Include(t => t.User) .Where(t => t.UserId == users[1].Id && t.Revoked == false) @@ -75,7 +75,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Refresh_WhenCalledWithRevokedRefreshToken_ItShouldReturn400WithProblemDetails() { - var (user, refreshToken) = await ExecuteAsync(static async (context, ct, sp) => + var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var tokenGenerator = sp.GetRequiredService(); @@ -108,7 +108,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Refresh_WhenCalledWithExpiredRefreshToken_ItShouldReturn400WithProblemDetails() { - var (user, refreshToken) = await ExecuteAsync(static async (context, ct, sp) => + var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var tokenGenerator = sp.GetRequiredService(); @@ -142,7 +142,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi) [InlineData(-5)] public async Task Refresh_WhenCalledWithValidRefreshTokenAndExpiredOrNotExpiredAccessToken_ItShouldReturn200WithNewTokensAndSetRefreshCookie(int accessTokenExpiresAtOffset) { - var (user, refreshToken) = await ExecuteAsync(static async (context, ct, sp) => + var (user, refreshToken) = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var tokenGenerator = sp.GetRequiredService(); @@ -174,7 +174,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi) response.Should().HaveSetCookieHeader("fiscalos_refresh_cookie"); await response.Should().BeJsonContentOfType(HttpStatusCode.OK); - var oldRefreshTokenInDb = await ExecuteAsync( + var oldRefreshTokenInDb = await Api.ExecuteAsync( async (context, ct) => await context.Set() .Include(t => t.User) .Where(t => t.UserId == user.Id && t.Token == refreshToken.Token && t.Revoked == true) @@ -184,7 +184,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi) oldRefreshTokenInDb.Should().NotBeNull(); - var newRefreshTokenInDb = await ExecuteAsync( + var newRefreshTokenInDb = await Api.ExecuteAsync( async (context, ct) => await context.Set() .Include(t => t.User) .Where(t => t.UserId == user.Id && t.Revoked == false && t.Token != refreshToken.Token) diff --git a/tests/FiscalOS.API.Tests/Integration/Institutions/ConnectTests.cs b/tests/FiscalOS.API.Tests/Integration/Institutions/ConnectTests.cs index 476f11a..6772448 100644 --- a/tests/FiscalOS.API.Tests/Integration/Institutions/ConnectTests.cs +++ b/tests/FiscalOS.API.Tests/Integration/Institutions/ConnectTests.cs @@ -92,7 +92,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Connect_WhenCalledWithPlaidInstitutionIdThatIsAlreadyConnected_ItShouldReturn409WithProblemDetails() { - var (user, institution) = await ExecuteAsync(static async (context, ct, sp) => + var (user, institution) = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); @@ -136,7 +136,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi) { var plaidInstitutionId = "ins_109508"; - var (user, publicToken) = await ExecuteAsync(async (context, ct, sp) => + var (user, publicToken) = await Api.ExecuteAsync(async (context, ct, sp) => { var plaidClient = sp.GetRequiredService(); var passwordHasher = sp.GetRequiredService(); @@ -171,7 +171,7 @@ public class ConnectTests(TestApi testApi) : IntegrationTest(testApi) response.StatusCode.Should().Be(HttpStatusCode.OK); - var updatedUser = await ExecuteAsync( + var updatedUser = await Api.ExecuteAsync( async (context, ct) => await context.Set() .Include(u => u.Institutions) .ThenInclude(i => i.Metadata) diff --git a/tests/FiscalOS.API.Tests/Integration/Institutions/GetAvailableTests.cs b/tests/FiscalOS.API.Tests/Integration/Institutions/GetAvailableTests.cs index 81e72d9..7ccb8db 100644 --- a/tests/FiscalOS.API.Tests/Integration/Institutions/GetAvailableTests.cs +++ b/tests/FiscalOS.API.Tests/Integration/Institutions/GetAvailableTests.cs @@ -39,7 +39,7 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task GetAvailable_WhenCalledWithNonExistentInstitutionId_ItShouldReturn404WithProblemDetails() { - var user = await ExecuteAsync(static async (context, ct, sp) => + var user = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); @@ -65,7 +65,7 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task GetAvailable_WhenCalledWithConnectedInstitution_ItShouldReturn200WithAvailableAccounts() { - var (user, institution, expectedAccounts) = await ExecuteAsync(static async (context, ct, sp) => + var (user, institution, expectedAccounts) = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); @@ -93,10 +93,10 @@ public class GetAvailableTests(TestApi testApi) : IntegrationTest(testApi) }); var encryptedAccessToken = await encryptor.EncryptAsyncFor(user, exchangeTokenResponse.AccessToken, ct); var institutionMetadata = PlaidInstitutionMetadata.From( - institutionId, - "Some Bank", - encryptedAccessToken, - exchangeTokenResponse.ItemId + institutionId, + "Some Bank", + encryptedAccessToken, + exchangeTokenResponse.ItemId ); var institution = Institution.From("Some Bank", institutionMetadata); diff --git a/tests/FiscalOS.API.Tests/Integration/Institutions/GetTests.cs b/tests/FiscalOS.API.Tests/Integration/Institutions/GetTests.cs index 1ca190e..f5da2aa 100644 --- a/tests/FiscalOS.API.Tests/Integration/Institutions/GetTests.cs +++ b/tests/FiscalOS.API.Tests/Integration/Institutions/GetTests.cs @@ -36,7 +36,7 @@ public class GetTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Get_WhenCalledByUser_ItShouldReturn200WithListOfInstitutions() { - var (user, institution) = await ExecuteAsync(static async (context, ct, sp) => + var (user, institution) = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); diff --git a/tests/FiscalOS.API.Tests/Integration/Institutions/LinkTests.cs b/tests/FiscalOS.API.Tests/Integration/Institutions/LinkTests.cs index 35af04a..7387357 100644 --- a/tests/FiscalOS.API.Tests/Integration/Institutions/LinkTests.cs +++ b/tests/FiscalOS.API.Tests/Integration/Institutions/LinkTests.cs @@ -32,7 +32,7 @@ public class LinkTests(TestApi testApi) : IntegrationTest(testApi) [Fact] public async Task Link_WhenCalledWithUserWhoExists_ItShouldReturnLinkToken() { - var user = await ExecuteAsync(static async (context, ct, sp) => + var user = await Api.ExecuteAsync(static async (context, ct, sp) => { var passwordHasher = sp.GetRequiredService(); var encryptor = sp.GetRequiredService(); diff --git a/tests/FiscalOS.API.Tests/Integration/IntegrationTest.cs b/tests/FiscalOS.API.Tests/Integration/IntegrationTest.cs index b89211b..bf17ca3 100644 --- a/tests/FiscalOS.API.Tests/Integration/IntegrationTest.cs +++ b/tests/FiscalOS.API.Tests/Integration/IntegrationTest.cs @@ -2,51 +2,17 @@ namespace FiscalOS.API.Tests.Integration; public abstract class IntegrationTest(TestApi testApi) : IClassFixture, IAsyncLifetime { - public HttpClient Client => testApi.CreateClient(); + protected TestApi Api => testApi; + protected HttpClient Client => testApi.CreateClient(); public async ValueTask InitializeAsync() { - await ExecuteAsync(static async (context, ct) => - { - await context.Database.EnsureCreatedAsync(ct); - }, TestContext.Current.CancellationToken); - } - - protected async Task ExecuteAsync(Func action, CancellationToken ct) - { - await using var scope = testApi.Services.CreateAsyncScope(); - var context = scope.ServiceProvider.GetRequiredService(); - await action(context, ct); - } - - protected async Task ExecuteAsync(Func action, CancellationToken ct) - { - await using var scope = testApi.Services.CreateAsyncScope(); - var context = scope.ServiceProvider.GetRequiredService(); - await action(context, ct, scope.ServiceProvider); - } - - protected async Task ExecuteAsync(Func> action, CancellationToken ct) - { - await using var scope = testApi.Services.CreateAsyncScope(); - var context = scope.ServiceProvider.GetRequiredService(); - return await action(context, ct); - } - - protected async Task ExecuteAsync(Func> action, CancellationToken ct) - { - await using var scope = testApi.Services.CreateAsyncScope(); - var context = scope.ServiceProvider.GetRequiredService(); - return await action(context, ct, scope.ServiceProvider); + await Api.EnsureDbCreatedAsync(); } public async ValueTask DisposeAsync() { - await ExecuteAsync(static async (context, ct) => - { - await context.Database.EnsureDeletedAsync(ct); - }, TestContext.Current.CancellationToken); - + await Api.EnsureDbDeletedAsync(); GC.SuppressFinalize(this); } } \ No newline at end of file diff --git a/tests/FiscalOS.Infra.Tests/Unit/PlaidTransactionSyncerTests.cs b/tests/FiscalOS.Infra.Tests/Unit/PlaidTransactionSyncerTests.cs index 36904f2..0f20207 100644 --- a/tests/FiscalOS.Infra.Tests/Unit/PlaidTransactionSyncerTests.cs +++ b/tests/FiscalOS.Infra.Tests/Unit/PlaidTransactionSyncerTests.cs @@ -2,12 +2,4 @@ namespace FiscalOS.Infra.Tests.Unit; public class PlaidTransactionSyncerTests { - private readonly Mock> _mockSyncer = new(); - private - private readonly PlaidTransactionSyncer _sut; - - public PlaidTransactionSyncer() - { - _sut = PlaidTransactionSyncer.From(); - } -} +} \ No newline at end of file