using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using ScadaLink.Commons.Entities.Security; using ScadaLink.Commons.Interfaces.Repositories; using ScadaLink.Commons.Interfaces.Services; using ScadaLink.ConfigurationDatabase; namespace ScadaLink.IntegrationTests; /// /// WP-22: Audit transactional guarantee — entity change + audit log in same transaction. /// public class AuditTransactionTests : IClassFixture { private readonly ScadaLinkWebApplicationFactory _factory; public AuditTransactionTests(ScadaLinkWebApplicationFactory factory) { _factory = factory; } [Fact] public async Task AuditLog_IsCommittedWithEntityChange_InSameTransaction() { using var scope = _factory.Services.CreateScope(); var securityRepo = scope.ServiceProvider.GetRequiredService(); var auditService = scope.ServiceProvider.GetRequiredService(); var dbContext = scope.ServiceProvider.GetRequiredService(); // Add a mapping and an audit log entry in the same unit of work var mapping = new LdapGroupMapping("test-group-audit", "Admin"); await securityRepo.AddMappingAsync(mapping); await auditService.LogAsync( user: "test-user", action: "Create", entityType: "LdapGroupMapping", entityId: "0", // ID not yet assigned entityName: "test-group-audit", afterState: new { Group = "test-group-audit", Role = "Admin" }); // Both should be in the change tracker before saving var trackedEntities = dbContext.ChangeTracker.Entries().Count(e => e.State == EntityState.Added); Assert.True(trackedEntities >= 2, "Both entity and audit log should be tracked before SaveChanges"); // Single SaveChangesAsync commits both await securityRepo.SaveChangesAsync(); // Verify both were persisted var mappings = await securityRepo.GetAllMappingsAsync(); Assert.Contains(mappings, m => m.LdapGroupName == "test-group-audit"); var auditEntries = await dbContext.AuditLogEntries.ToListAsync(); Assert.Contains(auditEntries, a => a.EntityName == "test-group-audit" && a.Action == "Create"); } [Fact] public async Task AuditLog_IsNotPersistedWhenSaveNotCalled() { // Create a separate scope so we have a fresh DbContext using var scope1 = _factory.Services.CreateScope(); var securityRepo = scope1.ServiceProvider.GetRequiredService(); var auditService = scope1.ServiceProvider.GetRequiredService(); // Add entity + audit but do NOT call SaveChangesAsync var mapping = new LdapGroupMapping("orphan-group", "Design"); await securityRepo.AddMappingAsync(mapping); await auditService.LogAsync("test", "Create", "LdapGroupMapping", "0", "orphan-group", null); // Dispose scope without saving — simulates a failed transaction scope1.Dispose(); // In a new scope, verify nothing was persisted using var scope2 = _factory.Services.CreateScope(); var securityRepo2 = scope2.ServiceProvider.GetRequiredService(); var dbContext2 = scope2.ServiceProvider.GetRequiredService(); var mappings = await securityRepo2.GetAllMappingsAsync(); Assert.DoesNotContain(mappings, m => m.LdapGroupName == "orphan-group"); var auditEntries = await dbContext2.AuditLogEntries.ToListAsync(); Assert.DoesNotContain(auditEntries, a => a.EntityName == "orphan-group"); } }