using Omika.Diagnostics.Referential; using Omika.Diagnostics.Tests.Fakes; namespace Omika.Diagnostics.Tests; /// /// T24-A Partie B — référentiel administrable : normalisation TRIM+UPPER, unicité /// (type, code), code immuable, archivage sans DELETE, libellés et codes non référencés. /// S'appuie sur le (même contrat que le /// repository Npgsql). /// public class DiagnosticReferenceTests { private static DiagnosticReferenceRecord Ref( long id, string type, string code, string label, bool enabled = true, int sortOrder = 0) => new() { Id = id, ReferenceType = type, Code = code, Label = label, Enabled = enabled, SortOrder = sortOrder, CreatedAtUtc = DateTimeOffset.UtcNow, UpdatedAtUtc = DateTimeOffset.UtcNow }; // ------------------------------------------- normalisation ---- [Theory] [InlineData("agenda", "AGENDA")] [InlineData(" agenda ", "AGENDA")] [InlineData("Facture_Detail", "FACTURE_DETAIL")] [InlineData(null, null)] [InlineData("", null)] [InlineData(" ", null)] public void NormalizeCode_TrimsAndUppers(string? input, string? expected) { Assert.Equal(expected, DiagnosticReferenceNormalization.NormalizeCode(input)); } // ------------------------------------------- création + normalisation ---- [Fact] public async Task Create_NormalizesCodeAndLabel() { var repo = new FakeDiagnosticReferenceRepository(); DiagnosticReferenceRecord? created = await repo.CreateAsync( DiagnosticReferenceTypes.ScreenCode, " facture_detail ", " Détail de facture ", null, true, 0); Assert.NotNull(created); Assert.Equal("FACTURE_DETAIL", created!.Code); Assert.Equal("Détail de facture", created.Label); } // ------------------------------------------- unicité (type, code) ---- [Fact] public async Task Create_DuplicateTypeCode_CaseInsensitive_ReturnsNull() { var repo = new FakeDiagnosticReferenceRepository(); repo.Seed(Ref(1, DiagnosticReferenceTypes.ScreenCode, "AGENDA", "Agenda")); DiagnosticReferenceRecord? duplicate = await repo.CreateAsync( DiagnosticReferenceTypes.ScreenCode, "agenda", "Agenda bis", null, true, 0); Assert.Null(duplicate); Assert.Single(await repo.ListAsync()); } [Fact] public async Task Create_SameCodeDifferentType_IsAllowed() { var repo = new FakeDiagnosticReferenceRepository(); repo.Seed(Ref(1, DiagnosticReferenceTypes.ScreenCode, "AGENDA", "Agenda")); // Même code mais type différent → autorisé (unicité = type + code). DiagnosticReferenceRecord? feature = await repo.CreateAsync( DiagnosticReferenceTypes.FeatureCode, "agenda", "Agenda (fonctionnalité)", null, true, 0); Assert.NotNull(feature); Assert.Equal(2, (await repo.ListAsync()).Count); } // ------------------------------------------- type invalide ---- [Fact] public async Task Create_UnsupportedType_ReturnsNull() { var repo = new FakeDiagnosticReferenceRepository(); DiagnosticReferenceRecord? created = await repo.CreateAsync( "EVENT_CODE", "X", "Libellé", null, true, 0); Assert.Null(created); } // ------------------------------------------- code immuable ---- [Fact] public async Task Update_ChangesLabelAndState_ButNeverCode() { var repo = new FakeDiagnosticReferenceRepository(); repo.Seed(Ref(1, DiagnosticReferenceTypes.ScreenCode, "AGENDA", "Agenda")); bool updated = await repo.UpdateAsync(1, "Nouvel Agenda", "Description", false, 7); Assert.True(updated); DiagnosticReferenceRecord? record = await repo.GetByIdAsync(1); Assert.NotNull(record); Assert.Equal("AGENDA", record!.Code); // code immuable. Assert.Equal("Nouvel Agenda", record.Label); Assert.False(record.Enabled); Assert.Equal(7, record.SortOrder); } // ------------------------------------------- archivage sans DELETE ---- [Fact] public async Task Archive_DisablesWithoutDelete() { var repo = new FakeDiagnosticReferenceRepository(); repo.Seed(Ref(1, DiagnosticReferenceTypes.ScreenCode, "AGENDA", "Agenda")); bool updated = await repo.UpdateAsync(1, "Agenda", null, enabled: false, sortOrder: 0); Assert.True(updated); Assert.False((await repo.GetByIdAsync(1))!.Enabled); // Aucun DELETE : l'entrée reste listée (état Archivé affiché, jamais masqué). Assert.Single(await repo.ListAsync()); } // ------------------------------------------- libellés dropdown ---- [Fact] public async Task GetLabels_ReturnsCodeToLabelIncludingArchived() { var repo = new FakeDiagnosticReferenceRepository(); repo.Seed( Ref(1, DiagnosticReferenceTypes.ScreenCode, "AGENDA", "Agenda"), Ref(2, DiagnosticReferenceTypes.ScreenCode, "SITE_LIST", "Sites", enabled: false), Ref(3, DiagnosticReferenceTypes.FeatureCode, "EMAIL_SEND", "Envoi de courriel")); IReadOnlyDictionary labels = await repo.GetLabelsAsync(DiagnosticReferenceTypes.ScreenCode); Assert.Equal("Agenda", labels["AGENDA"]); Assert.Equal("Sites", labels["SITE_LIST"]); // archivé inclus (libellé historique utile). Assert.False(labels.ContainsKey("EMAIL_SEND")); } // ------------------------------------------- codes non référencés ---- [Fact] public async Task GetUnreferenced_ReturnsObservedWithoutReference() { var repo = new FakeDiagnosticReferenceRepository(); repo.Seed(Ref(1, DiagnosticReferenceTypes.ScreenCode, "AGENDA", "Agenda")); DateTimeOffset last = DateTimeOffset.UtcNow.AddDays(-1); repo.SeedObserved(DiagnosticReferenceTypes.ScreenCode, "AGENDA", 40, last); // référencé → exclu. repo.SeedObserved(DiagnosticReferenceTypes.ScreenCode, "FACTURE_DETAIL", 128, last); // non référencé. repo.SeedObserved(DiagnosticReferenceTypes.FeatureCode, "FACTURE_EXPORT_PDF", 12, last); IReadOnlyList unreferenced = await repo.GetUnreferencedAsync(100); Assert.Equal(2, unreferenced.Count); Assert.Contains(unreferenced, u => u.ReferenceType == DiagnosticReferenceTypes.ScreenCode && u.Code == "FACTURE_DETAIL"); Assert.Contains(unreferenced, u => u.ReferenceType == DiagnosticReferenceTypes.FeatureCode && u.Code == "FACTURE_EXPORT_PDF"); Assert.DoesNotContain(unreferenced, u => u.Code == "AGENDA"); } }