2026-02-13 11:05:57 -06:00
using Account = FiscalOS . Core . Accounts . Account ;
2026-02-13 12:05:01 -06:00
using Institution = FiscalOS . Core . Accounts . Institution ;
2026-02-13 11:05:57 -06:00
2026-02-13 21:20:05 -06:00
namespace FiscalOS.API.Tests.Integration.Accounts ;
2026-02-13 11:05:57 -06:00
public class AddTests ( TestApi testApi ) : IntegrationTest ( testApi )
{
private static readonly Uri AddUri = new ( "/accounts" , UriKind . Relative );
[Fact]
public async Task Add_WhenCalledWhenNotLoggedIn_ItShouldReturn401WithProblemDetails ()
{
var response = await Client . PostAsync ( AddUri , null , TestContext . Current . CancellationToken );
await response . Should (). BeProblemDetails ( HttpStatusCode . Unauthorized );
}
[Fact]
2026-02-14 06:44:37 -06:00
public async Task Add_WhenCalledWithoutRequiredInformation_ItShouldReturn400WithProblemDetails ()
2026-02-13 11:05:57 -06:00
{
2026-02-13 21:20:05 -06:00
using var request = HttpRequestBuilder . New ()
. Post ( AddUri )
. WithUserId ( Guid . NewGuid ())
. WithBody ( new { })
2026-02-13 11:05:57 -06:00
. Build ();
var response = await Client . SendAsync ( request , TestContext . Current . CancellationToken );
await response . Should (). BeValidationProblemDetails ( new Dictionary < string , string []>()
{
["PlaidInstitutionId"] = [ "The PlaidInstitutionId field is required." ],
["PlaidAccountId"] = [ "The PlaidAccountId field is required." ],
2026-02-13 12:05:01 -06:00
["PlaidAccountName"] = [ "The PlaidAccountName field is required." ],
2026-02-14 06:44:37 -06:00
["AccountCurrencyCode"] = [ "The AccountCurrencyCode field is required." ],
2026-02-13 11:05:57 -06:00
});
}
[Fact]
public async Task Add_WhenCalledWithoutInstitutionId_ItShouldReturn400WithProblemDetails ()
{
2026-02-13 21:20:05 -06:00
using var request = HttpRequestBuilder . New ()
. Post ( AddUri )
. WithUserId ( Guid . NewGuid ())
. WithBody ( new
{
plaidAccountId = "accountId" ,
plaidAccountName = "Some Account" ,
2026-02-14 06:44:37 -06:00
accountCurrencyCode = "USD" ,
2026-02-13 21:20:05 -06:00
})
2026-02-13 11:05:57 -06:00
. Build ();
var response = await Client . SendAsync ( request , TestContext . Current . CancellationToken );
await response . Should (). BeValidationProblemDetails ( new Dictionary < string , string []>()
{
["PlaidInstitutionId"] = [ "The PlaidInstitutionId field is required." ],
});
}
[Fact]
public async Task Add_WhenCalledWithoutAccountId_ItShouldReturn400WithProblemDetails ()
{
2026-02-13 21:20:05 -06:00
using var request = HttpRequestBuilder . New ()
. Post ( AddUri )
. WithUserId ( Guid . NewGuid ())
. WithBody ( new
{
plaidInstitutionId = "institutionId" ,
plaidAccountName = "Some Account" ,
2026-02-14 06:44:37 -06:00
accountCurrencyCode = "USD" ,
2026-02-13 21:20:05 -06:00
})
2026-02-13 11:05:57 -06:00
. Build ();
var response = await Client . SendAsync ( request , TestContext . Current . CancellationToken );
await response . Should (). BeValidationProblemDetails ( new Dictionary < string , string []>()
{
["PlaidAccountId"] = [ "The PlaidAccountId field is required." ],
});
}
2026-02-13 12:05:01 -06:00
[Fact]
public async Task Add_WhenCalledWithoutAccountName_ItShouldReturn400WithProblemDetails ()
{
2026-02-13 21:20:05 -06:00
using var request = HttpRequestBuilder . New ()
. Post ( AddUri )
. WithUserId ( Guid . NewGuid ())
. WithBody ( new
{
plaidInstitutionId = "institutionId" ,
plaidAccountId = "accountId" ,
2026-02-14 06:44:37 -06:00
accountCurrencyCode = "USD" ,
2026-02-13 21:20:05 -06:00
})
2026-02-13 12:05:01 -06:00
. Build ();
var response = await Client . SendAsync ( request , TestContext . Current . CancellationToken );
await response . Should (). BeValidationProblemDetails ( new Dictionary < string , string []>()
{
["PlaidAccountName"] = [ "The PlaidAccountName field is required." ],
});
}
2026-02-13 11:05:57 -06:00
[Fact]
public async Task Add_WhenCalledWithNonExistentUser_ItShouldReturn401WithProblemDetails ()
{
2026-02-13 21:20:05 -06:00
using var request = HttpRequestBuilder . New ()
. Post ( AddUri )
. WithUserId ( Guid . NewGuid ())
. WithBody ( new
{
plaidInstitutionId = "id" ,
plaidAccountId = "id" ,
plaidAccountName = "Some Account" ,
2026-02-14 06:44:37 -06:00
accountCurrencyCode = "USD" ,
2026-02-13 21:20:05 -06:00
})
2026-02-13 11:05:57 -06:00
. Build ();
var response = await Client . SendAsync ( request , TestContext . Current . CancellationToken );
await response . Should (). BeProblemDetails ( HttpStatusCode . Unauthorized );
}
2026-02-13 12:05:01 -06:00
[Fact]
public async Task Add_WhenCalledWithPlaidInstitutionIdThatHasNotBeenAdded_ItShouldReturn400WithProblemDetails ()
{
var user = await ExecuteAsync ( static async ( context , ct , sp ) =>
{
var passwordHasher = sp . GetRequiredService < IPasswordHasher >();
var encryptor = sp . GetRequiredService < IEncryptor >();
var userEncryptionKey = await encryptor . GenerateEncryptedKeyAsync ( ct );
var user = User . From ( "User1" , passwordHasher . Hash ( "@Password1" ), userEncryptionKey );
await context . AddAsync ( user , ct );
await context . SaveChangesAsync ( ct );
return user ;
}, TestContext . Current . CancellationToken );
2026-02-13 21:20:05 -06:00
using var request = HttpRequestBuilder . New ()
. Post ( AddUri )
. WithUserId ( user . Id )
. WithBody ( new
{
plaidInstitutionId = "id" ,
plaidAccountId = "id" ,
plaidAccountName = "Some Account" ,
2026-02-14 06:44:37 -06:00
accountCurrencyCode = "USD" ,
2026-02-13 21:20:05 -06:00
})
2026-02-13 12:05:01 -06:00
. Build ();
var response = await Client . SendAsync ( request , TestContext . Current . CancellationToken );
await response . Should (). BeValidationProblemDetails ( new Dictionary < string , string []>()
{
["PlaidInstitutionId"] = [ "The PlaidInstitutionId field is invalid. No institution connected with the given PlaidInstitutionId was found for the user." ],
});
}
2026-02-13 11:05:57 -06:00
[Fact]
public async Task Add_WhenCalledWithPlaidAccountIdThatHasAlreadyBeenAdded_ItShouldReturn409WithProblemDetails ()
{
var ( user , institution , account ) = await ExecuteAsync ( static async ( context , ct , sp ) =>
{
var passwordHasher = sp . GetRequiredService < IPasswordHasher >();
var encryptor = sp . GetRequiredService < IEncryptor >();
var userEncryptionKey = await encryptor . GenerateEncryptedKeyAsync ( ct );
var user = User . From ( "User1" , passwordHasher . Hash ( "@Password1" ), userEncryptionKey );
await context . AddAsync ( user , ct );
var encryptedAccessToken = await encryptor . EncryptAsyncFor ( user , "accessToken" , ct );
var plaidMetadata = PlaidMetadata . From ( "alreadyExists" , "Some Bank" , encryptedAccessToken );
var institution = Institution . From ( "Some Bank" , plaidMetadata );
await context . AddAsync ( institution , ct );
var plaidAccountMetadata = PlaidAccountMetadata . From ( "accountId" , "Some Account" );
var account = Account . From ( institution . Id , "Some Account" , plaidAccountMetadata );
user . AddInstitution ( institution );
user . AddAccount ( account );
await context . SaveChangesAsync ( ct );
return ( user , institution , account );
}, TestContext . Current . CancellationToken );
2026-02-13 21:20:05 -06:00
using var request = HttpRequestBuilder . New ()
. Post ( AddUri )
. WithUserId ( user . Id )
. WithBody ( new
{
plaidInstitutionId = (( PlaidMetadata ) institution . Metadata !). PlaidId ,
plaidAccountId = (( PlaidAccountMetadata ) account . Metadata !). PlaidId ,
plaidAccountName = (( PlaidAccountMetadata ) account . Metadata ). PlaidName ,
2026-02-14 06:44:37 -06:00
accountCurrencyCode = "USD" ,
2026-02-13 21:20:05 -06:00
})
2026-02-13 11:05:57 -06:00
. Build ();
var response = await Client . SendAsync ( request , TestContext . Current . CancellationToken );
await response . Should (). BeProblemDetails ( HttpStatusCode . Conflict );
}
2026-02-13 12:05:01 -06:00
[Fact]
public async Task Add_WhenCalledWithNewAccount_ItShouldReturn200 ()
{
var ( user , institution ) = await ExecuteAsync ( async ( context , ct , sp ) =>
{
var passwordHasher = sp . GetRequiredService < IPasswordHasher >();
var encryptor = sp . GetRequiredService < IEncryptor >();
var plaidClient = sp . GetRequiredService < PlaidClient >();
var userEncryptionKey = await encryptor . GenerateEncryptedKeyAsync ( ct );
var user = User . From ( "User1" , passwordHasher . Hash ( "@Password1" ), userEncryptionKey );
var encryptedAccessToken = await encryptor . EncryptAsyncFor ( user , "accessToken" , ct );
var plaidMetadata = PlaidMetadata . From ( "id" , "Some Bank" , encryptedAccessToken );
var institution = Institution . From ( "Some Bank" , plaidMetadata );
user . AddInstitution ( institution );
await context . AddAsync ( user , ct );
await context . SaveChangesAsync ( ct );
return ( user , institution );
}, TestContext . Current . CancellationToken );
var newAccountId = "newAccountId" ;
var newAccountName = "New Account" ;
2026-02-14 05:54:12 -06:00
var expectedBalance = 100 ;
2026-02-14 06:44:37 -06:00
var expectedCurrencyCode = "USD" ;
2026-02-13 12:05:01 -06:00
2026-02-13 21:20:05 -06:00
using var request = HttpRequestBuilder . New ()
. Post ( AddUri )
. WithUserId ( user . Id )
. WithBody ( new
{
plaidInstitutionId = (( PlaidMetadata ) institution . Metadata !). PlaidId ,
plaidAccountId = newAccountId ,
plaidAccountName = newAccountName ,
2026-02-14 05:54:12 -06:00
accountCurrentBalance = expectedBalance ,
accountAvailableBalance = expectedBalance ,
2026-02-14 06:44:37 -06:00
accountCurrencyCode = expectedCurrencyCode ,
2026-02-13 21:20:05 -06:00
})
. Build ();
2026-02-13 12:05:01 -06:00
var response = await Client . SendAsync ( request , TestContext . Current . CancellationToken );
response . StatusCode . Should (). Be ( HttpStatusCode . OK );
var updatedUser = await ExecuteAsync (
async ( context , ct ) => await context . Set < User >()
. Include ( u => u . Accounts )
. ThenInclude ( a => a . Metadata )
2026-02-14 05:54:12 -06:00
. Include ( u => u . Accounts )
. ThenInclude ( a => a . Balances )
. AsSplitQuery ()
2026-02-13 12:05:01 -06:00
. FirstAsync ( u => u . Id == user . Id , ct ),
TestContext . Current . CancellationToken
);
updatedUser . Accounts . Should (). ContainSingle (
a => a . Name == newAccountName &&
a . InstitutionId == institution . Id &&
a . Metadata is PlaidAccountMetadata &&
(( PlaidAccountMetadata ) a . Metadata ). PlaidId == newAccountId &&
(( PlaidAccountMetadata ) a . Metadata ). PlaidName == newAccountName
);
2026-02-14 05:54:12 -06:00
updatedUser . Accounts . First ()
. Balances . Should (). ContainSingle (
b => b . AccountId == updatedUser . Accounts . First (). Id &&
b . Current == expectedBalance &&
2026-02-14 06:44:37 -06:00
b . Available == expectedBalance &&
b . CurrencyCode == expectedCurrencyCode
2026-02-14 05:54:12 -06:00
);
2026-02-13 12:05:01 -06:00
}
2026-02-13 11:05:57 -06:00
}