namespace KArtSell.Integration.Tests.ApprovalWorkflow; using System; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; using KArtSell.BuildingBlocks.Time; using KArtSell.Modules.ModelOperations.ApprovalWorkflow; public class ApprovalWorkflowTests : IAsyncLifetime { private readonly string _connectionString; private readonly ApprovalSql _sql; private readonly ApprovalPolicy _policy; private readonly IOutbox _outbox; public ApprovalWorkflowTests() { _connectionString = "Host=localhost;Port=5432;Database=kartselldb;Username=kartsell;Password=kartsell4321@!"; _sql = new ApprovalSql(_connectionString, new SystemClock()); _policy = new ApprovalPolicy(new SystemClock()); _outbox = new InMemoryOutbox(); } public async Task InitializeAsync() { // Ensure database is ready await Task.CompletedTask; } public async Task DisposeAsync() { await Task.CompletedTask; } [Fact] public void CanCreateProposal_WithMakerRole_ReturnsTrue() { // Arrange var makerEmail = "maker@company.com"; var makerRole = "Maker"; // Act var result = _policy.CanCreateProposal(makerEmail, makerRole); // Assert Assert.True(result); } [Fact] public void CanCreateProposal_WithoutMakerRole_ReturnsFalse() { // Arrange var email = "user@company.com"; var role = "Viewer"; // Act var result = _policy.CanCreateProposal(email, role); // Assert Assert.False(result); } [Fact] public void CanApproveApproval_WithDifferentChecker_ReturnsTrue() { // Arrange var maker = "maker@company.com"; var checker = "checker@company.com"; var proposal = new ApprovalProposal { CreatedBy = maker, Status = ApprovalStatus.Proposed }; // Act var result = _policy.CanApproveApproval(proposal, checker, maker); // Assert Assert.True(result); } [Fact] public void CanApproveApproval_WithSameMaker_ReturnsFalse() { // Arrange var maker = "maker@company.com"; var proposal = new ApprovalProposal { CreatedBy = maker, Status = ApprovalStatus.Proposed }; // Act var result = _policy.CanApproveApproval(proposal, maker, maker); // Assert Assert.False(result); } [Fact] public void CreateProposal_SetsCorrectDefaults() { // Arrange var modelId = Guid.NewGuid(); var maker = "maker@company.com"; var justification = "Model passed OOS testing"; var effectiveAt = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(7)); // Act var proposal = _policy.CreateProposal(modelId, maker, justification, effectiveAt); // Assert Assert.Equal(modelId, proposal.ModelId); Assert.Equal(maker, proposal.CreatedBy); Assert.Equal(ApprovalStatus.Draft, proposal.Status); Assert.Equal(justification, proposal.Justification); Assert.Equal(effectiveAt, proposal.EffectiveAt); } [Fact] public void ProposeApproval_TransitionsToProposed() { // Arrange var proposal = new ApprovalProposal { Id = Guid.NewGuid(), CreatedBy = "maker@company.com", Status = ApprovalStatus.Draft, Justification = "Test", EffectiveAt = DateOnly.FromDateTime(DateTime.UtcNow) }; // Act var updated = _policy.ProposeApproval(proposal, "maker@company.com"); // Assert Assert.Equal(ApprovalStatus.Proposed, updated.Status); Assert.NotNull(updated.ProposedAt); } [Fact] public void ApproveApproval_AddsEvidence() { // Arrange var proposal = new ApprovalProposal { Id = Guid.NewGuid(), CreatedBy = "maker@company.com", Status = ApprovalStatus.Proposed, Evidence = [], CorrelationId = Guid.NewGuid() }; var evidence = new List { new() { Type = "PBO_SCORE", Url = "s3://pbo-0.95.json", Comment = "Verified" } }; // Act var updated = _policy.ApproveApproval(proposal, "checker@company.com", "Looks good", evidence); // Assert Assert.Equal(ApprovalStatus.Approved, updated.Status); Assert.Equal("checker@company.com", updated.ApprovedBy); Assert.Single(updated.Evidence); Assert.Equal("PBO_SCORE", updated.Evidence[0].EvidenceType); } [Fact] public async Task InsertAndRetrieveProposal_RoundTrips() { // Arrange var id = Guid.NewGuid(); var modelId = Guid.NewGuid(); var correlationId = Guid.NewGuid(); // Act await _sql.InsertProposalAsync( id, modelId, "Draft", "maker@company.com", "Test justification", DateOnly.FromDateTime(DateTime.UtcNow), DateTimeOffset.UtcNow, correlationId); var retrieved = await _sql.GetProposalByIdAsync(id, DateTimeOffset.UtcNow.AddDays(1)); // Assert Assert.NotNull(retrieved); Assert.Equal(id, retrieved.Id); Assert.Equal(modelId, retrieved.ModelId); Assert.Equal(correlationId, retrieved.CorrelationId); } } public class InMemoryOutbox : IOutbox { public List<(string EventType, Guid CorrelationId, object Data)> Events { get; } = []; public Task PublishAsync(string eventType, Guid correlationId, object data) { Events.Add((eventType, correlationId, data)); return Task.CompletedTask; } }