Backend (.NET 10): - EventFlow CQRS/Event Sourcing with PostgreSQL - GraphQL.NET API with mutations and queries - Custom ReadModelSqlGenerator for snake_case PostgreSQL columns - Hangfire for background job processing - Integration tests with isolated test databases Frontend (React/Vite): - Initial project structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
260 lines
8.3 KiB
C#
260 lines
8.3 KiB
C#
using Books.Api.Domain.Companies;
|
|
using Books.Api.EventFlow.Repositories;
|
|
using Books.Api.Tests.Helpers;
|
|
using Books.Api.Tests.Infrastructure;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace Books.Api.Tests.GraphQL;
|
|
|
|
/// <summary>
|
|
/// Integration tests for Company GraphQL operations.
|
|
/// Each test class runs with its own isolated database.
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
public class CompanyGraphQLTests(TestWebApplicationFactory factory)
|
|
: IntegrationTestBase(factory)
|
|
{
|
|
[Fact]
|
|
public async Task Query_Companies_ReturnsEmptyList_WhenNoCompaniesExist()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
|
|
// Act
|
|
var response = await graphqlClient.QueryAsync<CompaniesResponse>("""
|
|
query {
|
|
companies {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
""");
|
|
|
|
// Assert
|
|
response.EnsureNoErrors();
|
|
response.Data.Should().NotBeNull();
|
|
response.Data!.Companies.Should().BeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_CreateCompany_CreatesCompanySuccessfully()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
|
|
// Act
|
|
var response = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
|
|
mutation CreateCompany($input: CreateCompanyInput!) {
|
|
createCompany(input: $input) {
|
|
id
|
|
name
|
|
cvr
|
|
country
|
|
currency
|
|
fiscalYearStartMonth
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
name = "Test Virksomhed A/S",
|
|
cvr = "12345678",
|
|
country = "DK",
|
|
currency = "DKK",
|
|
fiscalYearStartMonth = 1
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
response.EnsureNoErrors();
|
|
response.Data.Should().NotBeNull();
|
|
response.Data!.CreateCompany.Should().NotBeNull();
|
|
response.Data.CreateCompany!.Name.Should().Be("Test Virksomhed A/S");
|
|
response.Data.CreateCompany.Cvr.Should().Be("12345678");
|
|
response.Data.CreateCompany.Country.Should().Be("DK");
|
|
response.Data.CreateCompany.Currency.Should().Be("DKK");
|
|
response.Data.CreateCompany.FiscalYearStartMonth.Should().Be(1);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Query_Company_ReturnsCompany_AfterCreation()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
|
|
// Create a company first
|
|
var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
|
|
mutation CreateCompany($input: CreateCompanyInput!) {
|
|
createCompany(input: $input) {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
input = new
|
|
{
|
|
name = "Query Test Virksomhed",
|
|
cvr = "87654321"
|
|
}
|
|
});
|
|
|
|
createResponse.EnsureNoErrors();
|
|
var companyId = createResponse.Data!.CreateCompany!.Id;
|
|
|
|
// Act - Query the company by ID (with eventual consistency)
|
|
var company = await Eventually.GetAsync(async () =>
|
|
{
|
|
var response = await graphqlClient.QueryAsync<CompanyResponse>("""
|
|
query GetCompany($id: ID!) {
|
|
company(id: $id) {
|
|
id
|
|
name
|
|
cvr
|
|
}
|
|
}
|
|
""",
|
|
new { id = companyId });
|
|
|
|
return response.Data?.Company;
|
|
});
|
|
|
|
// Assert
|
|
company.Should().NotBeNull();
|
|
company!.Name.Should().Be("Query Test Virksomhed");
|
|
company.Cvr.Should().Be("87654321");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Query_Companies_ReturnsAllCompanies_AfterMultipleCreations()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
|
|
// Create multiple companies with unique names for this test
|
|
var company1Name = $"ListTest-Virksomhed-{Guid.NewGuid():N}";
|
|
var company2Name = $"ListTest-Virksomhed-{Guid.NewGuid():N}";
|
|
|
|
await graphqlClient.MutateAsync<CreateCompanyResponse>($$"""
|
|
mutation { createCompany(input: { name: "{{company1Name}}" }) { id } }
|
|
""");
|
|
|
|
await graphqlClient.MutateAsync<CreateCompanyResponse>($$"""
|
|
mutation { createCompany(input: { name: "{{company2Name}}" }) { id } }
|
|
""");
|
|
|
|
// Act - Wait for eventual consistency (at least 2 companies with our prefix)
|
|
var companies = await Eventually.GetAsync(
|
|
async () =>
|
|
{
|
|
var response = await graphqlClient.QueryAsync<CompaniesResponse>("""
|
|
query {
|
|
companies {
|
|
id
|
|
name
|
|
}
|
|
}
|
|
""");
|
|
var allCompanies = response.Data?.Companies ?? [];
|
|
var ourCompanies = allCompanies.Where(c => c.Name.StartsWith("ListTest-")).ToList();
|
|
return ourCompanies.Count >= 2 ? ourCompanies : null;
|
|
});
|
|
|
|
// Assert - verify our specific companies are present
|
|
companies.Should().Contain(c => c.Name == company1Name);
|
|
companies.Should().Contain(c => c.Name == company2Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Mutation_UpdateCompany_UpdatesCompanySuccessfully()
|
|
{
|
|
// Arrange
|
|
var graphqlClient = new GraphQLTestClient(Client);
|
|
|
|
// Create a company first
|
|
var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
|
|
mutation { createCompany(input: { name: "Original Name" }) { id } }
|
|
""");
|
|
|
|
createResponse.EnsureNoErrors();
|
|
var companyId = createResponse.Data!.CreateCompany!.Id;
|
|
|
|
// Wait for the company to be created
|
|
await Eventually.GetAsync(async () =>
|
|
{
|
|
var repository = GetService<ICompanyRepository>();
|
|
return await repository.GetByIds([CompanyId.With(companyId)]);
|
|
});
|
|
|
|
// Act - Update the company
|
|
var updateResponse = await graphqlClient.MutateAsync<UpdateCompanyResponse>("""
|
|
mutation UpdateCompany($id: ID!, $input: UpdateCompanyInput!) {
|
|
updateCompany(id: $id, input: $input) {
|
|
id
|
|
name
|
|
cvr
|
|
}
|
|
}
|
|
""",
|
|
new
|
|
{
|
|
id = companyId,
|
|
input = new
|
|
{
|
|
name = "Updated Name",
|
|
cvr = "11111111"
|
|
}
|
|
});
|
|
|
|
// Assert
|
|
updateResponse.EnsureNoErrors();
|
|
|
|
// Wait for eventual consistency and verify update
|
|
var updatedCompany = await Eventually.GetAsync(async () =>
|
|
{
|
|
var repository = GetService<ICompanyRepository>();
|
|
var companies = await repository.GetByIds([CompanyId.With(companyId)]);
|
|
var company = companies.First();
|
|
return company?.Name == "Updated Name" ? company : null;
|
|
});
|
|
|
|
updatedCompany.Should().NotBeNull();
|
|
updatedCompany!.Name.Should().Be("Updated Name");
|
|
updatedCompany.Cvr.Should().Be("11111111");
|
|
}
|
|
|
|
// Response DTOs for deserialization
|
|
private class CompaniesResponse
|
|
{
|
|
public List<CompanyDto> Companies { get; set; } = [];
|
|
}
|
|
|
|
private class CompanyResponse
|
|
{
|
|
public CompanyDto? Company { get; set; }
|
|
}
|
|
|
|
private class CreateCompanyResponse
|
|
{
|
|
public CompanyDto? CreateCompany { get; set; }
|
|
}
|
|
|
|
private class UpdateCompanyResponse
|
|
{
|
|
public CompanyDto? UpdateCompany { get; set; }
|
|
}
|
|
|
|
private class CompanyDto
|
|
{
|
|
public string Id { get; set; } = string.Empty;
|
|
public string Name { get; set; } = string.Empty;
|
|
public string? Cvr { get; set; }
|
|
public string? Country { get; set; }
|
|
public string? Currency { get; set; }
|
|
public int? FiscalYearStartMonth { get; set; }
|
|
}
|
|
}
|