tests(infra,api): refactor to allow further config of TestApi
This commit is contained in:
+1
-2
@@ -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
|
||||
|
||||
@@ -13,7 +13,10 @@ internal sealed class HttpRequestBuilder
|
||||
{
|
||||
}
|
||||
|
||||
public static HttpRequestBuilder New() => new();
|
||||
public static HttpRequestBuilder New()
|
||||
{
|
||||
return new();
|
||||
}
|
||||
|
||||
public HttpRequestBuilder WithMethod(HttpMethod method)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,8 @@ namespace FiscalOS.API.Tests.Infra;
|
||||
|
||||
public class TestApi : WebApplicationFactory<Program>
|
||||
{
|
||||
private readonly List<Action<IWebHostBuilder>> _additionalConfigs = [];
|
||||
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
base.ConfigureWebHost(builder);
|
||||
@@ -25,5 +27,71 @@ public class TestApi : WebApplicationFactory<Program>
|
||||
|
||||
c.AddSingleton<IKeyRing>(TestKeyRing.From);
|
||||
});
|
||||
|
||||
foreach (var config in _additionalConfigs)
|
||||
{
|
||||
config(builder);
|
||||
}
|
||||
}
|
||||
|
||||
public TestApi WithAdditionalConfig(Action<IWebHostBuilder> configuration)
|
||||
{
|
||||
var newApi = new TestApi();
|
||||
|
||||
newApi._additionalConfigs.AddRange(_additionalConfigs);
|
||||
newApi._additionalConfigs.Add(configuration);
|
||||
|
||||
return newApi;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(Func<DbContext, CancellationToken, Task> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await action(context, ct);
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(Func<DbContext, CancellationToken, IServiceProvider, Task> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await action(context, ct, scope.ServiceProvider);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteAsync<T>(Func<DbContext, CancellationToken, Task<T>> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await action(context, ct);
|
||||
}
|
||||
|
||||
public async Task<T> ExecuteAsync<T>(Func<DbContext, CancellationToken, IServiceProvider, Task<T>> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -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<IAsyncQueue<SyncUpdatesQueueItem>>();
|
||||
|
||||
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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -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<User>()
|
||||
.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<SyncUpdatesQueueItem>(),
|
||||
It.IsAny<CancellationToken>()
|
||||
), Times.Once());
|
||||
}
|
||||
}
|
||||
@@ -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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
|
||||
@@ -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<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
@@ -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<RefreshToken>()
|
||||
.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<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
@@ -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<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
@@ -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<IPasswordHasher>();
|
||||
var tokenGenerator = sp.GetRequiredService<ITokenGenerator>();
|
||||
@@ -174,7 +174,7 @@ public class RefreshTests(TestApi testApi) : IntegrationTest(testApi)
|
||||
response.Should().HaveSetCookieHeader("fiscalos_refresh_cookie");
|
||||
await response.Should().BeJsonContentOfType<API.Auth.Refresh.Response>(HttpStatusCode.OK);
|
||||
|
||||
var oldRefreshTokenInDb = await ExecuteAsync(
|
||||
var oldRefreshTokenInDb = await Api.ExecuteAsync(
|
||||
async (context, ct) => await context.Set<RefreshToken>()
|
||||
.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<RefreshToken>()
|
||||
.Include(t => t.User)
|
||||
.Where(t => t.UserId == user.Id && t.Revoked == false && t.Token != refreshToken.Token)
|
||||
|
||||
@@ -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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -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<PlaidClient>();
|
||||
var passwordHasher = sp.GetRequiredService<IPasswordHasher>();
|
||||
@@ -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<User>()
|
||||
.Include(u => u.Institutions)
|
||||
.ThenInclude(i => i.Metadata)
|
||||
|
||||
@@ -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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
@@ -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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
|
||||
@@ -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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
|
||||
@@ -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<IPasswordHasher>();
|
||||
var encryptor = sp.GetRequiredService<IEncryptor>();
|
||||
|
||||
@@ -2,51 +2,17 @@ namespace FiscalOS.API.Tests.Integration;
|
||||
|
||||
public abstract class IntegrationTest(TestApi testApi) : IClassFixture<TestApi>, 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<DbContext, CancellationToken, Task> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = testApi.Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await action(context, ct);
|
||||
}
|
||||
|
||||
protected async Task ExecuteAsync(Func<DbContext, CancellationToken, IServiceProvider, Task> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = testApi.Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
await action(context, ct, scope.ServiceProvider);
|
||||
}
|
||||
|
||||
protected async Task<T> ExecuteAsync<T>(Func<DbContext, CancellationToken, Task<T>> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = testApi.Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return await action(context, ct);
|
||||
}
|
||||
|
||||
protected async Task<T> ExecuteAsync<T>(Func<DbContext, CancellationToken, IServiceProvider, Task<T>> action, CancellationToken ct)
|
||||
{
|
||||
await using var scope = testApi.Services.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,4 @@ namespace FiscalOS.Infra.Tests.Unit;
|
||||
|
||||
public class PlaidTransactionSyncerTests
|
||||
{
|
||||
private readonly Mock<ILogger<PlaidTransactionSyncer>> _mockSyncer = new();
|
||||
private
|
||||
private readonly PlaidTransactionSyncer _sut;
|
||||
|
||||
public PlaidTransactionSyncer()
|
||||
{
|
||||
_sut = PlaidTransactionSyncer.From();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user