using System.Net.Http.Json;
using ItWorx.KnowledgeHub.Contracts.Knowledge;
using ItWorx.KnowledgeHub.Domain.Automation;
using ItWorx.KnowledgeHub.Infrastructure.Persistence;
using ItWorx.KnowledgeHub.IntegrationTests.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace ItWorx.KnowledgeHub.IntegrationTests.Automation;
/// Every mutation this milestone wires into the outbox must produce a
/// durable, correctly-typed row in the same transaction as the mutation itself
/// (docs/24-mcp-readiness.md "Outbox events", M6-A06) — proven here by reading the
/// table directly, the same "no consumer exists yet, but the record must be there and
/// correct for one later" contract the outbox is designed around.
[Collection(PostgresTestGroup.Name)]
public sealed class OutboxTests(PostgresContainerFixture postgres) : OwnerSetupTestBase(postgres)
{
[Fact]
public async Task Creating_a_page_records_a_page_created_outbox_event_in_the_same_transaction()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var createResponse = await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/pages", new CreatePageRequest(null, "Outbox-testpagina", "WikiPage", "Inhoud.", null));
var page = await createResponse.Content.ReadFromJsonAsync();
Assert.NotNull(page);
using var scope = Factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
var messages = await db.OutboxMessages
.Where(message => message.EventType == "page.created.v1" && message.AggregateId == page.Id.ToString())
.ToListAsync();
var message = Assert.Single(messages);
Assert.False(message.IsProcessed);
Assert.Contains(page.Id.ToString(), message.PayloadJson, StringComparison.Ordinal);
}
[Fact]
public async Task Publishing_a_page_edit_records_a_page_updated_outbox_event()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
var createResponse = await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/pages", new CreatePageRequest(null, "Bij te werken pagina", "WikiPage", "Inhoud.", null));
var page = await createResponse.Content.ReadFromJsonAsync();
Assert.NotNull(page);
await SetupHelper.PutWithCsrfAsync(
client, $"/api/v1/pages/{page.Id}",
new UpdatePageRequest("Bijgewerkte titel", "Nieuwe inhoud", "WikiPage", "Active", null), page.ETag);
using var scope = Factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService();
var messages = await db.OutboxMessages
.Where(message => message.EventType == "page.updated.v1" && message.AggregateId == page.Id.ToString())
.ToListAsync();
Assert.Single(messages);
}
[Fact]
public async Task Unprocessed_outbox_messages_are_queryable_oldest_first_a_future_dispatcher_could_replay_them()
{
using var client = Factory.CreateClient();
await SetupHelper.LoginAsync(client, OwnerEmail, OwnerPassword);
await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/pages", new CreatePageRequest(null, "Herstel-test pagina 1", "WikiPage", "I.", null));
await SetupHelper.PostWithCsrfAsync(
client, "/api/v1/pages", new CreatePageRequest(null, "Herstel-test pagina 2", "WikiPage", "I.", null));
using var scope = Factory.Services.CreateScope();
var outbox = scope.ServiceProvider.GetRequiredService();
var unprocessed = await outbox.GetUnprocessedAsync(500, CancellationToken.None);
Assert.True(unprocessed.Count >= 2);
Assert.All(unprocessed, message => Assert.False(message.IsProcessed));
// Oldest first: monotonically non-decreasing AvailableAt.
for (var i = 1; i < unprocessed.Count; i++)
{
Assert.True(unprocessed[i].AvailableAt >= unprocessed[i - 1].AvailableAt);
}
}
}