using Manager.DTOs; using ManagerService.Data; using ManagerService.Data.SubSection; using ManagerService.DTOs; using ManagerService.Tests.Infrastructure; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.Json; using Xunit; namespace ManagerService.Tests.Data { /// /// Le journal d'audit ne voyait aucune section : le filtre exigeait l'égalité exacte /// de type, or est abstraite. Ces tests tiennent l'invariant — /// un sous-type est journalisé, et il l'est sous le nom « Section ». /// public class AuditLogTests { private static SectionArticle NewArticle(string id = "s1") => TestSection.Article(id, "i1", "Label", "c1"); [Fact] public void SectionSubType_IsAudited() { using var db = DbContextFactory.Create(); db.Sections.Add(NewArticle()); db.SaveChanges(); var log = Assert.Single(db.AuditLogs.ToList()); Assert.Equal("Create", log.Action); Assert.Equal("s1", log.EntityId); Assert.Equal("i1", log.InstanceId); } [Fact] public void SectionSubType_IsLoggedUnderBaseTypeName() { using var db = DbContextFactory.Create(); db.Sections.Add(NewArticle()); db.SaveChanges(); // Le filtre « Section » de l'écran d'audit interroge ce nom : s'il portait // « SectionArticle », l'écran ne rendrait toujours aucune ligne. Assert.Equal("Section", db.AuditLogs.Single().EntityType); } [Fact] public void SectionSubType_KeepsConcreteTypeInValues() { using var db = DbContextFactory.Create(); db.Sections.Add(NewArticle()); db.SaveChanges(); // Normaliser EntityType ne perd pas le sous-type : le discriminateur TPH est // une propriété du modèle, donc sérialisée avec les autres. var values = JsonSerializer.Deserialize>( db.AuditLogs.Single().NewValues!); Assert.Equal("Article", values!["Discriminator"].GetString()); } [Fact] public void SectionUpdate_RecordsOnlyChangedFields() { using var db = DbContextFactory.Create(); db.Sections.Add(NewArticle()); db.SaveChanges(); var section = db.Sections.Single(); section.Label = "Renommée"; db.SaveChanges(); var update = db.AuditLogs.Single(a => a.Action == "Update"); var oldValues = JsonSerializer.Deserialize>(update.OldValues!); var newValues = JsonSerializer.Deserialize>(update.NewValues!); // DateUpdate est estampillé à chaque SaveChanges : il n'a rien à faire dans le // diff, et son absence est ce qui rend cette assertion lisible. Assert.Equal("Label", Assert.Single(newValues!.Keys)); Assert.Equal("Label", oldValues!["Label"].GetString()); Assert.Equal("Renommée", newValues["Label"].GetString()); } // ── COLONNES MACHINE ───────────────────────────────────────────────── // // WeatherSyncService réécrit WeatherResult deux fois par jour, sur cron. Depuis que // les sections sont journalisées, chaque passage produisait une ligne portant la // prévision complète en avant ET en après — du bruit qui noie les modifications // humaines que l'écran d'audit existe pour montrer. private static SectionWeather NewWeatherSection() => new SectionWeather { Id = "w1", InstanceId = "i1", Label = "Météo", ConfigurationId = "c1", Type = SectionType.Weather, Title = new List(), Description = new List(), WeatherCity = "Namur" }; [Fact] public void WeatherRefresh_ProducesNoAuditEntry() { using var db = DbContextFactory.Create(); db.Sections.Add(NewWeatherSection()); db.SaveChanges(); var section = db.Sections.OfType().Single(); section.WeatherResult = "{\"list\":[/* 30 Ko de prévisions */]}"; section.WeatherUpdatedDate = DateTimeOffset.UtcNow; db.SaveChanges(); Assert.Empty(db.AuditLogs.Where(a => a.Action == "Update").ToList()); } [Fact] public void HumanEditOnAWeatherSection_IsStillAudited_WithoutTheForecast() { using var db = DbContextFactory.Create(); db.Sections.Add(NewWeatherSection()); db.SaveChanges(); var section = db.Sections.OfType().Single(); section.WeatherCity = "Liège"; section.WeatherResult = "{\"list\":[/* rafraîchi dans la foulée */]}"; db.SaveChanges(); var update = db.AuditLogs.Single(a => a.Action == "Update"); var newValues = JsonSerializer.Deserialize>(update.NewValues!); Assert.Equal("WeatherCity", Assert.Single(newValues!.Keys)); Assert.Equal("Liège", newValues["WeatherCity"].GetString()); } [Fact] public void SectionCreation_DoesNotCarryTheForecastBlob() { using var db = DbContextFactory.Create(); var section = NewWeatherSection(); section.WeatherResult = "{\"list\":[/* déjà rempli */]}"; db.Sections.Add(section); db.SaveChanges(); var values = JsonSerializer.Deserialize>( db.AuditLogs.Single().NewValues!); Assert.DoesNotContain("WeatherResult", values!.Keys); Assert.DoesNotContain("DateUpdate", values.Keys); Assert.Equal("Namur", values["WeatherCity"].GetString()); } [Fact] public void SectionDelete_IsAudited() { using var db = DbContextFactory.Create(); db.Sections.Add(NewArticle()); db.SaveChanges(); db.Sections.Remove(db.Sections.Single()); db.SaveChanges(); var delete = db.AuditLogs.Single(a => a.Action == "Delete"); Assert.Equal("Section", delete.EntityType); Assert.Equal("s1", delete.EntityId); } [Fact] public void NonAuditedEntity_IsNotLogged() { using var db = DbContextFactory.Create(); db.VisitEvents.Add(new VisitEvent { Id = "v1", InstanceId = "i1", SessionId = "sess", EventType = VisitEventType.SectionView, Timestamp = DateTime.UtcNow }); db.SaveChanges(); Assert.Empty(db.AuditLogs.ToList()); } [Fact] public void OtherAuditedTypes_KeepTheirOwnName() { using var db = DbContextFactory.Create(); db.Users.Add(new User { Id = "u1", InstanceId = "i1", Email = "a@b.c", FirstName = "A", LastName = "B", Password = "x", Token = "t" }); db.SaveChanges(); Assert.Equal("User", db.AuditLogs.Single().EntityType); } /// /// Le trou d'origine venait d'une liste de types que rien n'obligeait à suivre les /// sous-classes. Par réflexion : tout sous-type concret de Section doit être journalisé. /// [Fact] public void EverySectionSubType_ResolvesToSection() { var subTypes = typeof(Section).Assembly.GetTypes() .Where(t => typeof(Section).IsAssignableFrom(t) && !t.IsAbstract) .ToList(); Assert.Equal(13, subTypes.Count); var auditedTypeOf = typeof(MyInfoMateDbContext) .GetMethod("AuditedTypeOf", BindingFlags.NonPublic | BindingFlags.Static)!; foreach (var subType in subTypes) { var instance = System.Runtime.CompilerServices.RuntimeHelpers .GetUninitializedObject(subType); Assert.Equal(typeof(Section), auditedTypeOf.Invoke(null, new[] { instance })); } } } }