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();
// 14 depuis le 12/09 : SectionScene3D (maquette 3D, item E7 du lot XR-4).
// Ce chiffre est volontairement en dur — il force à passer ici, donc à
// vérifier que le nouveau sous-type est bien journalisé.
Assert.Equal(14, 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 }));
}
}
}
}