Files
Omika/tests/Omika.Diagnostics.Tests/DiagnosticReferenceTests.cs
T
2026-10-23 18:46:50 +02:00

169 lines
6.7 KiB
C#

using Omika.Diagnostics.Referential;
using Omika.Diagnostics.Tests.Fakes;
namespace Omika.Diagnostics.Tests;
/// <summary>
/// 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 <see cref="FakeDiagnosticReferenceRepository"/> (même contrat que le
/// repository Npgsql).
/// </summary>
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<string, string> 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<DiagnosticUnreferencedCode> 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");
}
}