using D3ROVoice.Api.Data; using D3ROVoice.Api.Services; using Microsoft.EntityFrameworkCore; using Xunit; namespace D3ROVoice.Api.Tests; public sealed class AdminOperationServiceTests { [Fact] public async Task MutationIsAtomicAuditedAndIdempotent() { var options = new DbContextOptionsBuilder() .UseSqlite("Data Source=:memory:") .Options; await using var db = new AppDbContext(options); await db.Database.OpenConnectionAsync(); await db.Database.EnsureCreatedAsync(); var service = new AdminOperationService(db); var idempotencyKey = Guid.NewGuid().ToString("D"); var mutationCalls = 0; async Task Mutate() { mutationCalls += 1; var endpoint = new ServiceModelEndpoint { ModelId = "real-model", ModelName = "Real Model", Provider = "Custom", EndpointUrl = "https://models.example.com/v1", ApiKey = "secret-never-in-response" }; db.ModelEndpoints.Add(endpoint); await db.SaveChangesAsync(); return new { endpoint.Id, endpoint.ModelId }; } var first = await service.ExecuteAsync( "ADMIN@EXAMPLE.COM", "model_endpoint.create", idempotencyKey, new { modelId = "real-model" }, "model_endpoint", _ => "real-model", "approved provider setup", () => Task.FromResult(null), Mutate); var replay = await service.ExecuteAsync( "admin@example.com", "model_endpoint.create", idempotencyKey, new { modelId = "real-model" }, "model_endpoint", _ => "real-model", "approved provider setup", () => Task.FromResult(null), Mutate); Assert.Equal(1, mutationCalls); Assert.Equal(first.GetRawText(), replay.GetRawText()); Assert.Equal(1, await db.ModelEndpoints.CountAsync()); Assert.Equal(1, await db.AdminOperationRequests.CountAsync()); var audit = await db.AdminAuditEntries.SingleAsync(); Assert.Equal("admin@example.com", audit.ActorEmail); Assert.Equal("model_endpoint.create", audit.Action); Assert.Equal(idempotencyKey, audit.IdempotencyKey); await Assert.ThrowsAsync(() => service.ExecuteAsync( "admin@example.com", "model_endpoint.create", idempotencyKey, new { modelId = "different-model" }, "model_endpoint", _ => "different-model", "different request", () => Task.FromResult(null), Mutate)); } [Fact] public async Task FailedMutationRollsBackDomainAndAuditWrites() { var options = new DbContextOptionsBuilder() .UseSqlite("Data Source=:memory:") .Options; await using var db = new AppDbContext(options); await db.Database.OpenConnectionAsync(); await db.Database.EnsureCreatedAsync(); var service = new AdminOperationService(db); async Task FailingMutation() { db.ModelEndpoints.Add(new ServiceModelEndpoint { ModelId = "rollback-model", ModelName = "Rollback Model", Provider = "Custom", EndpointUrl = "https://models.example.com/v1" }); await db.SaveChangesAsync(); throw new InvalidOperationException("simulated persistence failure"); } await Assert.ThrowsAsync(() => service.ExecuteAsync( "admin@example.com", "model_endpoint.create", Guid.NewGuid().ToString("D"), new { modelId = "rollback-model" }, "model_endpoint", _ => "rollback-model", "rollback verification", () => Task.FromResult(null), FailingMutation)); db.ChangeTracker.Clear(); Assert.Empty(await db.ModelEndpoints.ToListAsync()); Assert.Empty(await db.AdminOperationRequests.ToListAsync()); Assert.Empty(await db.AdminAuditEntries.ToListAsync()); } }