using Books.Api.EventFlow.Infrastructure;
using Books.Api.EventFlow.ReadModels;
using Books.Api.EventFlow.Repositories;
using Books.Api.Tests.Helpers;
using Books.Api.Tests.Infrastructure;
using Dapper;
using AwesomeAssertions;
using Microsoft.Extensions.DependencyInjection;
using Npgsql;
namespace Books.Api.Tests.Integration;
///
/// Integration tests for the read model auto-repair/repopulation system.
/// Tests verify that corrupt or out-of-sync read models can be repaired
/// by replaying events from the event store.
///
[Trait("Category", "Integration")]
public class ReadModelRepopulationTests(TestWebApplicationFactory factory)
: IntegrationTestBase(factory)
{
[Fact]
public async Task RepopulateReadModel_FixesCorruptedStatus()
{
// Arrange: Create a draft through normal flow
var graphqlClient = new GraphQLTestClient(Client);
var companyId = await CreateCompanyAsync(graphqlClient, "Repopulation Test Company");
var draftId = await CreateDraftAsync(graphqlClient, companyId, "Test Draft for Repopulation");
// Wait for read model to be created
var originalDraft = await Eventually.GetAsync(async () =>
{
var repo = GetService();
return await repo.GetByIdAsync(draftId);
});
originalDraft.Should().NotBeNull();
originalDraft!.Status.Should().Be("active");
// Corrupt the read model: Set status to something wrong
await CorruptReadModelStatusAsync(draftId, "posted");
// Verify corruption
var corruptedDraft = await GetService().GetByIdAsync(draftId);
corruptedDraft!.Status.Should().Be("posted"); // Corrupted!
// Act: Repopulate the read model
var response = await graphqlClient.MutateAsync("""
mutation Repopulate($aggregateId: String!, $readModelType: String!) {
repopulateReadModel(aggregateId: $aggregateId, readModelType: $readModelType)
}
""",
new
{
aggregateId = draftId,
readModelType = "JournalEntryDraftReadModel"
});
// Assert: Repopulation succeeded
response.EnsureNoErrors();
response.Data!.RepopulateReadModel.Should().BeTrue();
// Verify read model is now fixed
var fixedDraft = await GetService().GetByIdAsync(draftId);
fixedDraft!.Status.Should().Be("active"); // Fixed!
}
[Fact]
public async Task RepopulateReadModel_FixesMissingName()
{
// Arrange
var graphqlClient = new GraphQLTestClient(Client);
var companyId = await CreateCompanyAsync(graphqlClient, "Missing Name Test Company");
var draftId = await CreateDraftAsync(graphqlClient, companyId, "Original Draft Name");
await Eventually.GetAsync(async () =>
{
var repo = GetService();
return await repo.GetByIdAsync(draftId);
});
// Corrupt: Clear the name
await CorruptReadModelNameAsync(draftId, "");
var corruptedDraft = await GetService().GetByIdAsync(draftId);
corruptedDraft!.Name.Should().BeEmpty();
// Act
var response = await graphqlClient.MutateAsync("""
mutation Repopulate($aggregateId: String!, $readModelType: String!) {
repopulateReadModel(aggregateId: $aggregateId, readModelType: $readModelType)
}
""",
new
{
aggregateId = draftId,
readModelType = "JournalEntryDraftReadModel"
});
// Assert
response.EnsureNoErrors();
var fixedDraft = await GetService().GetByIdAsync(draftId);
fixedDraft!.Name.Should().Be("Original Draft Name");
}
[Fact]
public async Task RepopulateReadModel_RestoresUpdatedData()
{
// Arrange: Create and update a draft
var graphqlClient = new GraphQLTestClient(Client);
var companyId = await CreateCompanyAsync(graphqlClient, "Updated Data Test Company");
var draftId = await CreateDraftAsync(graphqlClient, companyId, "Initial Name");
await Eventually.GetAsync(async () =>
{
var repo = GetService();
return await repo.GetByIdAsync(draftId);
});
// Update the draft with new data
await graphqlClient.MutateAsync("""
mutation UpdateDraft($input: UpdateJournalEntryDraftInput!) {
updateJournalEntryDraft(input: $input) { id name description }
}
""",
new
{
input = new
{
id = draftId,
name = "Updated Name",
description = "Test Description",
documentDate = "2025-01-15",
lines = Array.Empty