This commit includes all previously untracked backend files: Domain: - Accounts, Attachments, BankConnections, Customers - FiscalYears, Invoices, JournalEntryDrafts - Orders, Products, UserAccess Commands & Handlers: - Full CQRS command structure for all domains Repositories: - PostgreSQL repositories for all read models - Bank transaction and ledger repositories GraphQL: - Input types, scalars, and types for all entities - Mutations and queries Infrastructure: - Banking integration (Enable Banking client) - File storage, Invoicing, Reporting, SAF-T export - Database migrations (003-029) Tests: - Integration tests for GraphQL endpoints - Domain tests - Invoicing and reporting tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
819 lines
30 KiB
C#
819 lines
30 KiB
C#
using Books.Api.EventFlow.Repositories;
|
|
using Books.Api.Tests.Helpers;
|
|
using Books.Api.Tests.Infrastructure;
|
|
using AwesomeAssertions;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace Books.Api.Tests.GraphQL;
|
|
|
|
/// <summary>
|
|
/// Integration tests for Account GraphQL operations.
|
|
/// Each test class runs with its own isolated database.
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
public class AccountGraphQLTests(TestWebApplicationFactory factory)
|
|
: IntegrationTestBase(factory)
|
|
{
|
|
[Fact]
|
|
public async Task Query_Accounts_FailsWithoutCompanyHeader()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
// Note: We're NOT setting X-Company-Id header
|
|
|
|
// Act - query accounts without X-Company-Id header
|
|
var response = await graphqlClient.QueryAsync<AccountsResponse>("""
|
|
query {
|
|
accounts(companyId: "nonexistent") {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
""");
|
|
|
|
// Assert - Should fail with authorization error
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
errorInfo.Should().Contain("No company selected");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_CreateAccount_CreatesAccountSuccessfully()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
|
|
// First create a company (which doesn't auto-create accounts in this test)
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Account Test Company");
|
|
|
|
// Act - Create an account
|
|
var response = await graphqlClient.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) {
|
|
id
|
|
companyId
|
|
accountNumber
|
|
name
|
|
accountType
|
|
isActive
|
|
isSystemAccount
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = "9999",
|
|
name = "Test Konto",
|
|
accountType = "EXPENSE",
|
|
description = "En testkonto"
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
response.EnsureNoErrors();
|
|
response.Data.Should().NotBeNull();
|
|
response.Data!.CreateAccount.Should().NotBeNull();
|
|
response.Data.CreateAccount!.AccountNumber.Should().Be("9999");
|
|
response.Data.CreateAccount.Name.Should().Be("Test Konto");
|
|
response.Data.CreateAccount.AccountType.Should().Be("expense");
|
|
response.Data.CreateAccount.IsActive.Should().BeTrue();
|
|
response.Data.CreateAccount.IsSystemAccount.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_UpdateAccount_UpdatesAccountSuccessfully()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Update Account Test");
|
|
|
|
// Create an account
|
|
var createResponse = await graphqlClient.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) { id name }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = "8888",
|
|
name = "Original Name",
|
|
accountType = "EXPENSE"
|
|
}
|
|
});
|
|
|
|
createResponse.EnsureNoErrors();
|
|
var accountId = createResponse.Data!.CreateAccount!.Id;
|
|
|
|
// Wait for eventual consistency
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetByIdAsync(accountId);
|
|
});
|
|
|
|
// Act - Update the account
|
|
var updateResponse = await graphqlClient.MutateAsync<UpdateAccountResponse>("""
|
|
mutation UpdateAccount($id: ID!, $input: UpdateAccountInput!) {
|
|
updateAccount(id: $id, input: $input) {
|
|
id
|
|
name
|
|
description
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
id = accountId,
|
|
input = new
|
|
{
|
|
name = "Updated Name",
|
|
description = "Updated description"
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
updateResponse.EnsureNoErrors();
|
|
|
|
var updatedAccount = await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
var account = await repo.GetByIdAsync(accountId);
|
|
return account?.Name == "Updated Name" ? account : null;
|
|
});
|
|
|
|
updatedAccount.Should().NotBeNull();
|
|
updatedAccount!.Name.Should().Be("Updated Name");
|
|
updatedAccount.Description.Should().Be("Updated description");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_DeactivateAccount_DeactivatesAccountSuccessfully()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Deactivate Account Test");
|
|
|
|
var createResponse = await graphqlClient.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) { id isActive }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = "7777",
|
|
name = "Deactivate Test",
|
|
accountType = "EXPENSE"
|
|
}
|
|
});
|
|
|
|
createResponse.EnsureNoErrors();
|
|
var accountId = createResponse.Data!.CreateAccount!.Id;
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetByIdAsync(accountId);
|
|
});
|
|
|
|
// Act
|
|
var deactivateResponse = await graphqlClient.MutateAsync<DeactivateAccountResponse>("""
|
|
mutation DeactivateAccount($id: ID!) {
|
|
deactivateAccount(id: $id) {
|
|
id
|
|
isActive
|
|
}
|
|
}
|
|
""",
|
|
new { id = accountId });
|
|
|
|
// Assert
|
|
deactivateResponse.EnsureNoErrors();
|
|
|
|
var deactivatedAccount = await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
var account = await repo.GetByIdAsync(accountId);
|
|
return account?.IsActive == false ? account : null;
|
|
});
|
|
|
|
deactivatedAccount.Should().NotBeNull();
|
|
deactivatedAccount!.IsActive.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_ReactivateAccount_ReactivatesAccountSuccessfully()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Reactivate Account Test");
|
|
|
|
var createResponse = await graphqlClient.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) { id }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = "6666",
|
|
name = "Reactivate Test",
|
|
accountType = "EXPENSE"
|
|
}
|
|
});
|
|
|
|
createResponse.EnsureNoErrors();
|
|
var accountId = createResponse.Data!.CreateAccount!.Id;
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetByIdAsync(accountId);
|
|
});
|
|
|
|
// Deactivate first
|
|
await graphqlClient.MutateAsync<DeactivateAccountResponse>("""
|
|
mutation { deactivateAccount(id: $id) { id } }
|
|
""".Replace("$id", $"\"{accountId}\""));
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
var account = await repo.GetByIdAsync(accountId);
|
|
return account?.IsActive == false ? account : null;
|
|
});
|
|
|
|
// Act
|
|
var reactivateResponse = await graphqlClient.MutateAsync<ReactivateAccountResponse>("""
|
|
mutation ReactivateAccount($id: ID!) {
|
|
reactivateAccount(id: $id) {
|
|
id
|
|
isActive
|
|
}
|
|
}
|
|
""",
|
|
new { id = accountId });
|
|
|
|
// Assert
|
|
reactivateResponse.EnsureNoErrors();
|
|
|
|
var reactivatedAccount = await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
var account = await repo.GetByIdAsync(accountId);
|
|
return account?.IsActive == true ? account : null;
|
|
});
|
|
|
|
reactivatedAccount.Should().NotBeNull();
|
|
reactivatedAccount!.IsActive.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Query_ActiveAccounts_ExcludesDeactivatedAccounts()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Active Accounts Test");
|
|
|
|
// Wait for standard accounts to be created
|
|
var initialAccounts = await Eventually.GetListAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetByCompanyIdAsync(companyId);
|
|
}, 50, timeout: TimeSpan.FromSeconds(30)); // At least 50 standard accounts
|
|
|
|
var initialActiveCount = initialAccounts.Count(a => a.IsActive);
|
|
|
|
// Create a custom account
|
|
var customAccountId = await CreateAccountAsync(graphqlClient, companyId, "5553", "Custom To Deactivate");
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetByIdAsync(customAccountId);
|
|
});
|
|
|
|
// Deactivate the custom account
|
|
await graphqlClient.MutateAsync<DeactivateAccountResponse>(
|
|
$"mutation {{ deactivateAccount(id: \"{customAccountId}\") {{ id }} }}");
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
var account = await repo.GetByIdAsync(customAccountId);
|
|
return account?.IsActive == false ? account : null;
|
|
});
|
|
|
|
// Act
|
|
var response = await graphqlClient.QueryAsync<ActiveAccountsResponse>("""
|
|
query ActiveAccounts($companyId: ID!) {
|
|
activeAccounts(companyId: $companyId) {
|
|
id
|
|
name
|
|
isActive
|
|
}
|
|
}
|
|
""",
|
|
new { companyId });
|
|
|
|
// Assert
|
|
response.EnsureNoErrors();
|
|
// Should have initial active accounts (deactivated one is excluded)
|
|
response.Data!.ActiveAccounts.Should().HaveCount(initialActiveCount);
|
|
response.Data.ActiveAccounts.Should().AllSatisfy(a => a.IsActive.Should().BeTrue());
|
|
response.Data.ActiveAccounts.Should().NotContain(a => a.Id == customAccountId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_CreateAccount_FailsWithInvalidAccountNumber()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Invalid Number Test");
|
|
|
|
// Act - Try to create account with invalid number (too short)
|
|
var response = await graphqlClient.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) {
|
|
id
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = "12", // Invalid - must be 4-10 digits
|
|
name = "Invalid Account",
|
|
accountType = "EXPENSE"
|
|
}
|
|
});
|
|
|
|
// Assert - Domain validation error is wrapped by GraphQL
|
|
response.HasErrors.Should().BeTrue();
|
|
// Check that the error is related to account validation (could be in message or extensions)
|
|
var errorDetails = response.Errors!.First().Extensions?.GetValueOrDefault("details")?.ToString() ?? "";
|
|
var hasAccountError = response.Errors!.Any(e =>
|
|
e.Message.Contains("INVALID_ACCOUNT_NUMBER") ||
|
|
e.Message.Contains("createAccount") ||
|
|
errorDetails.Contains("INVALID_ACCOUNT_NUMBER") ||
|
|
errorDetails.Contains("4-10 digits"));
|
|
hasAccountError.Should().BeTrue("Expected an error related to invalid account number");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_UpdateAccount_FailsForSystemAccount()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "System Account Update Test");
|
|
|
|
// Wait for chart of accounts to be initialized
|
|
var systemAccount = await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
var accounts = await repo.GetByCompanyIdAsync(companyId);
|
|
return accounts.FirstOrDefault(a => a.IsSystemAccount);
|
|
}, timeout: TimeSpan.FromSeconds(30));
|
|
|
|
systemAccount.Should().NotBeNull("Expected at least one system account from chart initialization");
|
|
|
|
// Act - Try to update a system account
|
|
var response = await graphqlClient.MutateAsync<UpdateAccountResponse>("""
|
|
mutation UpdateAccount($id: ID!, $input: UpdateAccountInput!) {
|
|
updateAccount(id: $id, input: $input) {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
id = systemAccount!.Id,
|
|
input = new { name = "Attempting to rename system account" }
|
|
});
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorDetails = response.Errors!.First().Extensions?.GetValueOrDefault("details")?.ToString() ?? "";
|
|
(response.Errors!.Any(e => e.Message.Contains("SYSTEM_ACCOUNT_READONLY")) ||
|
|
errorDetails.Contains("SYSTEM_ACCOUNT_READONLY") ||
|
|
errorDetails.Contains("System accounts cannot be modified")).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_DeactivateAccount_FailsForSystemAccount()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "System Account Deactivate Test");
|
|
|
|
// Wait for chart of accounts to be initialized
|
|
var systemAccount = await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
var accounts = await repo.GetByCompanyIdAsync(companyId);
|
|
return accounts.FirstOrDefault(a => a.IsSystemAccount);
|
|
}, timeout: TimeSpan.FromSeconds(30));
|
|
|
|
systemAccount.Should().NotBeNull("Expected at least one system account from chart initialization");
|
|
|
|
// Act - Try to deactivate a system account
|
|
var response = await graphqlClient.MutateAsync<DeactivateAccountResponse>("""
|
|
mutation DeactivateAccount($id: ID!) {
|
|
deactivateAccount(id: $id) {
|
|
id
|
|
isActive
|
|
}
|
|
}
|
|
""",
|
|
new { id = systemAccount!.Id });
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorDetails = response.Errors!.First().Extensions?.GetValueOrDefault("details")?.ToString() ?? "";
|
|
(response.Errors!.Any(e => e.Message.Contains("SYSTEM_ACCOUNT_READONLY")) ||
|
|
errorDetails.Contains("SYSTEM_ACCOUNT_READONLY") ||
|
|
errorDetails.Contains("System accounts cannot be deactivated")).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_DeactivateAccount_FailsWhenAlreadyInactive()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Double Deactivate Test");
|
|
var accountId = await CreateAccountAsync(graphqlClient, companyId, "4444", "Double Deactivate");
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetByIdAsync(accountId);
|
|
});
|
|
|
|
// First deactivation - should succeed
|
|
await graphqlClient.MutateAsync<DeactivateAccountResponse>("""
|
|
mutation DeactivateAccount($id: ID!) {
|
|
deactivateAccount(id: $id) { id }
|
|
}
|
|
""",
|
|
new { id = accountId });
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
var account = await repo.GetByIdAsync(accountId);
|
|
return account?.IsActive == false ? account : null;
|
|
});
|
|
|
|
// Act - Try to deactivate again
|
|
var response = await graphqlClient.MutateAsync<DeactivateAccountResponse>("""
|
|
mutation DeactivateAccount($id: ID!) {
|
|
deactivateAccount(id: $id) {
|
|
id
|
|
isActive
|
|
}
|
|
}
|
|
""",
|
|
new { id = accountId });
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorDetails = response.Errors!.First().Extensions?.GetValueOrDefault("details")?.ToString() ?? "";
|
|
(response.Errors!.Any(e => e.Message.Contains("ACCOUNT_ALREADY_INACTIVE")) ||
|
|
errorDetails.Contains("ACCOUNT_ALREADY_INACTIVE") ||
|
|
errorDetails.Contains("already inactive")).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_ReactivateAccount_FailsWhenAlreadyActive()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Reactivate Active Test");
|
|
var accountId = await CreateAccountAsync(graphqlClient, companyId, "3333", "Already Active");
|
|
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetByIdAsync(accountId);
|
|
});
|
|
|
|
// Act - Try to reactivate an already active account
|
|
var response = await graphqlClient.MutateAsync<ReactivateAccountResponse>("""
|
|
mutation ReactivateAccount($id: ID!) {
|
|
reactivateAccount(id: $id) {
|
|
id
|
|
isActive
|
|
}
|
|
}
|
|
""",
|
|
new { id = accountId });
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorDetails = response.Errors!.First().Extensions?.GetValueOrDefault("details")?.ToString() ?? "";
|
|
(response.Errors!.Any(e => e.Message.Contains("ACCOUNT_ALREADY_ACTIVE")) ||
|
|
errorDetails.Contains("ACCOUNT_ALREADY_ACTIVE") ||
|
|
errorDetails.Contains("already active")).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_CreateAccount_AcceptsMinimumDigits()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Min Digits Test");
|
|
|
|
// Act - Create account with exactly 4 digits (minimum allowed)
|
|
// Use a number not in the standard chart of accounts
|
|
var response = await graphqlClient.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) {
|
|
id
|
|
accountNumber
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = "9990", // Exactly 4 digits, not in standard chart
|
|
name = "Minimum Digits Account",
|
|
accountType = "ASSET"
|
|
}
|
|
});
|
|
|
|
// Assert - Should succeed
|
|
response.EnsureNoErrors();
|
|
response.Data!.CreateAccount!.AccountNumber.Should().Be("9990");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_CreateAccount_FailsWithTooFewDigits()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Too Few Digits Test");
|
|
|
|
// Act - Create account with 3 digits (below minimum)
|
|
var response = await graphqlClient.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) {
|
|
id
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = "999", // Only 3 digits
|
|
name = "Too Few Digits",
|
|
accountType = "EXPENSE"
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorDetails = response.Errors!.First().Extensions?.GetValueOrDefault("details")?.ToString() ?? "";
|
|
(response.Errors!.Any(e => e.Message.Contains("INVALID_ACCOUNT_NUMBER")) ||
|
|
errorDetails.Contains("INVALID_ACCOUNT_NUMBER") ||
|
|
errorDetails.Contains("4-10 digits")).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_CreateAccount_IsDeterministic()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var companyId = await CreateCompanyAsync(graphqlClient, "Deterministic ID Test");
|
|
|
|
// Act - Create same account twice (use number not in standard chart)
|
|
var response1 = await graphqlClient.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) {
|
|
id
|
|
accountNumber
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = "9991",
|
|
name = "Deterministic Test",
|
|
accountType = "EXPENSE"
|
|
}
|
|
});
|
|
|
|
response1.EnsureNoErrors();
|
|
var firstId = response1.Data!.CreateAccount!.Id;
|
|
|
|
// Wait for eventual consistency
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repo = GetService<IAccountRepository>();
|
|
return await repo.GetByIdAsync(firstId);
|
|
});
|
|
|
|
// Second attempt with same input
|
|
var response2 = await graphqlClient.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) {
|
|
id
|
|
accountNumber
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = "9991",
|
|
name = "Deterministic Test",
|
|
accountType = "EXPENSE"
|
|
}
|
|
});
|
|
|
|
// Assert - Second call should fail with ACCOUNT_EXISTS error (idempotency)
|
|
response2.HasErrors.Should().BeTrue();
|
|
var errorDetails = response2.Errors!.First().Extensions?.GetValueOrDefault("details")?.ToString() ?? "";
|
|
(response2.Errors!.Any(e => e.Message.Contains("ACCOUNT_EXISTS")) ||
|
|
errorDetails.Contains("ACCOUNT_EXISTS") ||
|
|
errorDetails.Contains("already exists")).Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_UpdateAccount_FailsForNonExistentAccount()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var nonExistentAccountId = $"account-{Guid.NewGuid():N}";
|
|
|
|
// Act - Try to update a non-existent account
|
|
var response = await graphqlClient.MutateAsync<UpdateAccountResponse>("""
|
|
mutation UpdateAccount($id: ID!, $input: UpdateAccountInput!) {
|
|
updateAccount(id: $id, input: $input) {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
id = nonExistentAccountId,
|
|
input = new { name = "New Name" }
|
|
});
|
|
|
|
// Assert - Domain exception is wrapped by GraphQL/EventFlow
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
(errorInfo.Contains("ACCOUNT_NOT_FOUND") ||
|
|
errorInfo.Contains("Account does not exist") ||
|
|
errorInfo.Contains("does not exist") ||
|
|
errorInfo.Contains("updateAccount")).Should().BeTrue(
|
|
$"Expected account not found error, got: {errorInfo}");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_DeactivateAccount_FailsForNonExistentAccount()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var nonExistentAccountId = $"account-{Guid.NewGuid():N}";
|
|
|
|
// Act - Try to deactivate a non-existent account
|
|
var response = await graphqlClient.MutateAsync<DeactivateAccountResponse>("""
|
|
mutation DeactivateAccount($id: ID!) {
|
|
deactivateAccount(id: $id) {
|
|
id
|
|
isActive
|
|
}
|
|
}
|
|
""",
|
|
new { id = nonExistentAccountId });
|
|
|
|
// Assert - Domain exception is wrapped by GraphQL/EventFlow
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
(errorInfo.Contains("ACCOUNT_NOT_FOUND") ||
|
|
errorInfo.Contains("Account does not exist") ||
|
|
errorInfo.Contains("does not exist") ||
|
|
errorInfo.Contains("deactivateAccount")).Should().BeTrue(
|
|
$"Expected account not found error, got: {errorInfo}");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_ReactivateAccount_FailsForNonExistentAccount()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
var nonExistentAccountId = $"account-{Guid.NewGuid():N}";
|
|
|
|
// Act - Try to reactivate a non-existent account
|
|
var response = await graphqlClient.MutateAsync<ReactivateAccountResponse>("""
|
|
mutation ReactivateAccount($id: ID!) {
|
|
reactivateAccount(id: $id) {
|
|
id
|
|
isActive
|
|
}
|
|
}
|
|
""",
|
|
new { id = nonExistentAccountId });
|
|
|
|
// Assert - Domain exception is wrapped by GraphQL/EventFlow
|
|
response.HasErrors.Should().BeTrue();
|
|
var errorInfo = string.Join(" ", response.Errors!.Select(e =>
|
|
e.Message + " " + (e.Extensions?.GetValueOrDefault("details")?.ToString() ?? "")));
|
|
(errorInfo.Contains("ACCOUNT_NOT_FOUND") ||
|
|
errorInfo.Contains("Account does not exist") ||
|
|
errorInfo.Contains("does not exist") ||
|
|
errorInfo.Contains("reactivateAccount")).Should().BeTrue(
|
|
$"Expected account not found error, got: {errorInfo}");
|
|
}
|
|
|
|
private async Task<string> CreateCompanyAsync(GraphQLTestClient client, string name)
|
|
{
|
|
var response = await client.MutateAsync<CreateCompanyResponse>("""
|
|
mutation CreateCompany($input: CreateCompanyInput!) {
|
|
createCompany(input: $input) { id }
|
|
}
|
|
""",
|
|
new { input = new { name } });
|
|
|
|
response.EnsureNoErrors();
|
|
var companyId = response.Data!.CreateCompany!.Id;
|
|
|
|
// Set the company ID header for subsequent requests
|
|
client.SetCompanyId(companyId);
|
|
|
|
return companyId;
|
|
}
|
|
|
|
private async Task<string> CreateAccountAsync(GraphQLTestClient client, string companyId, string number, string name)
|
|
{
|
|
var response = await client.MutateAsync<CreateAccountResponse>("""
|
|
mutation CreateAccount($input: CreateAccountInput!) {
|
|
createAccount(input: $input) { id }
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
companyId,
|
|
accountNumber = number,
|
|
name,
|
|
accountType = "EXPENSE"
|
|
}
|
|
});
|
|
|
|
response.EnsureNoErrors();
|
|
return response.Data!.CreateAccount!.Id;
|
|
}
|
|
|
|
// Response DTOs
|
|
private class AccountsResponse { public List<AccountDto> Accounts { get; set; } = []; }
|
|
private class ActiveAccountsResponse { public List<AccountDto> ActiveAccounts { get; set; } = []; }
|
|
private class AccountResponse { public AccountDto? Account { get; set; } }
|
|
private class CreateAccountResponse { public AccountDto? CreateAccount { get; set; } }
|
|
private class UpdateAccountResponse { public AccountDto? UpdateAccount { get; set; } }
|
|
private class DeactivateAccountResponse { public AccountDto? DeactivateAccount { get; set; } }
|
|
private class ReactivateAccountResponse { public AccountDto? ReactivateAccount { get; set; } }
|
|
private class CreateCompanyResponse { public CompanyDto? CreateCompany { get; set; } }
|
|
|
|
private class AccountDto
|
|
{
|
|
public string Id { get; set; } = string.Empty;
|
|
public string CompanyId { get; set; } = string.Empty;
|
|
public string AccountNumber { get; set; } = string.Empty;
|
|
public string Name { get; set; } = string.Empty;
|
|
public string AccountType { get; set; } = string.Empty;
|
|
public string? Description { get; set; }
|
|
public bool IsActive { get; set; }
|
|
public bool IsSystemAccount { get; set; }
|
|
}
|
|
|
|
private class CompanyDto { public string Id { get; set; } = string.Empty; }
|
|
}
|