using MaBibli.Api.Data;
using MaBibli.Shared.Entites;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
namespace MaBibli.Tests;
///
/// Rattrapage des colonnes normalisées après une migration.
///
///
/// Les données sont écrites telles que la migration SQL les laisse : normalisation
/// approchée (lower() ne touche pas aux accents), donc variantes d'un même auteur encore
/// éclatées. C'est exactement l'état que ce service doit savoir rattraper.
///
public class ServiceRenormalisationTests : IDisposable
{
private readonly SqliteConnection _connexion;
private readonly MaBibliDbContext _db;
public ServiceRenormalisationTests()
{
_connexion = new SqliteConnection("Data Source=:memory:");
_connexion.Open();
_db = new MaBibliDbContext(
new DbContextOptionsBuilder().UseSqlite(_connexion).Options);
_db.Database.EnsureCreated();
}
private Task ExecuterAsync() =>
new ServiceRenormalisation(_db, NullLogger.Instance).ExecuterAsync();
/// Écrit un auteur comme la migration le ferait : formes en lower() brut.
private Auteur AjouterHerite(string nom, params string[] titres)
{
var auteur = new Auteur
{
Nom = nom,
NomNormalise = nom.ToLowerInvariant(),
CleRegroupement = nom.ToLowerInvariant(),
};
_db.Auteurs.Add(auteur);
foreach (var titre in titres)
{
var livre = new Livre { Titre = titre, TitreNormalise = titre.ToLowerInvariant() };
_db.Livres.Add(livre);
livre.Auteurs.Add(new LivreAuteur { Auteur = auteur });
}
_db.SaveChanges();
return auteur;
}
[Fact]
public async Task Les_titres_perdent_leurs_accents_dans_la_colonne_normalisee()
{
AjouterHerite("Émile Zola", "La Bête humaine");
await ExecuterAsync();
Assert.Equal("la bete humaine", _db.Livres.Single().TitreNormalise);
}
[Fact]
public async Task Les_variantes_daccent_et_dordre_sont_reunies()
{
AjouterHerite("Emile Zola", "Germinal");
AjouterHerite("Émile Zola", "Nana");
AjouterHerite("ZOLA, Émile", "La Curée");
await ExecuterAsync();
var zola = Assert.Single(_db.Auteurs);
Assert.Equal(3, await _db.LivreAuteurs.CountAsync(la => la.AuteurId == zola.Id));
}
///
/// Le nom qui survit est celui qui se lit le mieux. Sans arbitrage, c'est l'ordre — arbitraire —
/// des identifiants issus de la migration qui déciderait, et la bibliothèque perdrait ses
/// accents au profit de la saisie la plus négligée.
///
[Theory]
// L'ordre est celui des identifiants après migration : le résultat doit en être indépendant.
[InlineData("Emile Zola|Émile Zola")]
[InlineData("Émile Zola|Emile Zola")]
[InlineData("EMILE ZOLA|ZOLA, Émile|Émile Zola")]
[InlineData("ZOLA, Émile|Émile Zola")]
public async Task Le_nom_conserve_est_le_plus_presentable(string variantes)
{
foreach (var variante in variantes.Split('|'))
{
AjouterHerite(variante, $"Livre de {variante}");
}
await ExecuterAsync();
Assert.Equal("Émile Zola", Assert.Single(_db.Auteurs).Nom);
}
///
/// Les initiales échappent à la clé de regroupement. Sans second tour, une base héritée
/// resterait éclatée là où une saisie neuve aurait été réunie d'emblée.
///
[Fact]
public async Task Les_initiales_abregees_sont_reunies_sous_le_nom_le_plus_complet()
{
AjouterHerite("P.F. Hamilton", "Pandora's Star");
AjouterHerite("Peter F. Hamilton", "Judas Unchained");
await ExecuterAsync();
var hamilton = Assert.Single(_db.Auteurs);
Assert.Equal("Peter F. Hamilton", hamilton.Nom);
Assert.Equal(2, await _db.LivreAuteurs.CountAsync());
}
/// Le cas ambigu reste ambigu : il sera proposé, pas appliqué.
[Fact]
public async Task Un_patronyme_seul_nest_jamais_absorbe()
{
AjouterHerite("Hamilton", "Great North Road");
AjouterHerite("Peter F. Hamilton", "Pandora's Star");
await ExecuterAsync();
Assert.Equal(2, await _db.Auteurs.CountAsync());
}
[Fact]
public async Task Un_livre_cosigne_par_deux_variantes_ne_garde_quun_lien()
{
var livre = new Livre { Titre = "Œuvre commune", TitreNormalise = "œuvre commune" };
_db.Livres.Add(livre);
_db.SaveChanges();
foreach (var nom in new[] { "Emile Zola", "Émile Zola" })
{
var auteur = new Auteur
{
Nom = nom,
NomNormalise = nom.ToLowerInvariant(),
CleRegroupement = nom.ToLowerInvariant(),
};
_db.Auteurs.Add(auteur);
_db.LivreAuteurs.Add(new LivreAuteur { LivreId = livre.Id, Auteur = auteur });
}
_db.SaveChanges();
await ExecuterAsync();
Assert.Single(_db.Auteurs);
Assert.Equal(1, await _db.LivreAuteurs.CountAsync());
}
[Fact]
public async Task Un_second_passage_ne_change_plus_rien()
{
AjouterHerite("Emile Zola", "Germinal");
AjouterHerite("Émile Zola", "Nana");
await ExecuterAsync();
var apresPremier = _db.Auteurs.Single();
var nom = apresPremier.Nom;
var cle = apresPremier.CleRegroupement;
await ExecuterAsync();
var apresSecond = Assert.Single(_db.Auteurs);
Assert.Equal(nom, apresSecond.Nom);
Assert.Equal(cle, apresSecond.CleRegroupement);
Assert.Equal(2, await _db.LivreAuteurs.CountAsync());
}
[Fact]
public async Task Une_base_vide_ne_pose_aucun_probleme()
{
await ExecuterAsync();
Assert.Empty(_db.Auteurs);
Assert.Empty(_db.Livres);
}
// ── Rattrapage des tables plus récentes ─────────────────────────────────
//
// Envies, séries, revues et numéros calculent leurs formes À L'ÉCRITURE : sans ce passage,
// un changement de règle de normalisation — comme le développement des ligatures le
// 2026-08-19 — laisserait ces lignes sur leur ancienne clé, donc introuvables par la
// nouvelle. Trois de ces colonnes portent en plus une UNICITÉ, et une collision se paierait
// au démarrage du serveur.
[Fact]
public async Task Une_envie_ecrite_avant_le_changement_de_regle_est_rattrapee()
{
// Forme telle qu'elle aurait été calculée avant le développement des ligatures.
_db.LivresSouhaites.Add(new LivreSouhaite
{
Utilisateur = "mathieu",
Titre = "L'Œuvre",
TitreNormalise = "l œuvre",
Auteur = "Émile Zola",
AuteurNormalise = "emile zola",
});
await _db.SaveChangesAsync();
await ExecuterAsync();
Assert.Equal("l oeuvre", _db.LivresSouhaites.Single().TitreNormalise);
}
[Fact]
public async Task Une_envie_de_revue_ecrite_avant_le_changement_de_regle_est_rattrapee()
{
// Formes telles qu'elles auraient été calculées avant le développement des ligatures,
// et ISSN saisi sans son tiret — deux rattrapages, deux mécanismes distincts.
_db.RevuesSouhaitees.Add(new RevueSouhaitee
{
Utilisateur = "mathieu",
Titre = "Cœur",
TitreNormalise = "cœur",
Numero = "Hors-Série 7",
NumeroNormalise = "hors-série 7",
Issn = "24666718",
});
await _db.SaveChangesAsync();
await ExecuterAsync();
var envie = _db.RevuesSouhaitees.Single();
Assert.Equal("coeur", envie.TitreNormalise);
Assert.Equal("hors serie 7", envie.NumeroNormalise);
// ⚠️ L'ISSN ne suit pas la clé : sur une ligne au titre inchangé, il ne serait jamais
// canonisé si ce rattrapage n'existait pas à part.
Assert.Equal("2466-6718", envie.Issn);
}
///
/// ⚠️ Une collision d'unicité au démarrage empêcherait le serveur de se lancer. La règle est
/// la même que partout ailleurs : la ligne la plus ancienne prend la nouvelle forme, les
/// suivantes gardent l'ancienne. Mal normalisée vaut infiniment mieux que supprimée.
///
[Fact]
public async Task Deux_envies_de_revue_que_la_nouvelle_regle_confond_survivent_toutes_les_deux()
{
_db.RevuesSouhaitees.AddRange(
new RevueSouhaitee
{
Utilisateur = "mathieu",
Titre = "Cœur",
TitreNormalise = "cœur",
},
new RevueSouhaitee
{
Utilisateur = "mathieu",
Titre = "Coeur",
TitreNormalise = "coeur",
});
await _db.SaveChangesAsync();
await ExecuterAsync();
var formes = _db.RevuesSouhaitees.OrderBy(e => e.Id).Select(e => e.TitreNormalise).ToList();
// La seconde garde « coeur » (elle l'avait déjà), la première ne peut pas la prendre.
Assert.Equal(2, formes.Count);
Assert.Equal("cœur", formes[0]);
Assert.Equal("coeur", formes[1]);
}
[Fact]
public async Task Une_serie_et_une_revue_sont_rattrapees_aussi()
{
_db.Series.Add(new Serie { Titre = "Les Sœurs", TitreNormalise = "les sœurs" });
_db.Revues.Add(new Revue { Titre = "Cœur", TitreNormalise = "cœur" });
await _db.SaveChangesAsync();
await ExecuterAsync();
Assert.Equal("les soeurs", _db.Series.Single().TitreNormalise);
Assert.Equal("coeur", _db.Revues.Single().TitreNormalise);
}
///
/// ⚠️ Le cas qui empêcherait le serveur de démarrer.
///
///
/// La nouvelle règle rend identiques deux séries qui ne l'étaient pas. L'index unique
/// refuserait l'écriture, et l'exception surviendrait au démarrage : l'application ne
/// se lancerait plus du tout. La plus ancienne prend la nouvelle forme, la suivante garde
/// l'ancienne — mal normalisée vaut infiniment mieux que supprimée, ou qu'un serveur mort.
///
[Fact]
public async Task Une_collision_dunicite_ne_fait_pas_echouer_le_demarrage()
{
_db.Series.Add(new Serie { Titre = "L'Œuvre", TitreNormalise = "l œuvre" });
_db.Series.Add(new Serie { Titre = "L'oeuvre", TitreNormalise = "l oeuvre" });
await _db.SaveChangesAsync();
await ExecuterAsync();
var series = _db.Series.OrderBy(s => s.Id).ToList();
Assert.Equal(2, series.Count);
Assert.Equal(["l oeuvre", "l œuvre"], series.Select(s => s.TitreNormalise).Order());
}
///
/// Une ligne déjà correcte ne doit pas se bloquer elle-même.
///
///
/// Le jeu des clés occupées part de l'état actuel de la base : sans la libération de sa
/// propre clé, toute ligne dont la forme change verrait sa nouvelle valeur « déjà prise »
/// dès qu'elle est égale à son ancienne — et rien ne serait jamais corrigé.
///
[Fact]
public async Task Le_rattrapage_reste_idempotent()
{
_db.Revues.Add(new Revue { Titre = "Cœur", TitreNormalise = "cœur" });
await _db.SaveChangesAsync();
await ExecuterAsync();
await ExecuterAsync();
Assert.Equal("coeur", _db.Revues.Single().TitreNormalise);
}
///
/// Les ISSN saisis avant le lot K2 prennent leur tiret au démarrage suivant, ce qui les rend
/// enfin rapprochables d'un code-barres et de bib.issn.
///
[Fact]
public async Task Les_issn_existants_prennent_leur_tiret()
{
_db.Revues.Add(new Revue { Titre = "Médor", TitreNormalise = "medor", Issn = "24666718" });
await _db.SaveChangesAsync();
await ExecuterAsync();
Assert.Equal("2466-6718", _db.Revues.Single().Issn);
}
///
/// ⚠️ L'unicité de l'ISSN est un index partiel en base : deux fiches que la
/// canonisation rendrait identiques feraient échouer l'écriture — au démarrage, donc
/// avant que l'application ne réponde. La seconde garde donc sa forme, et rien ne meurt.
///
[Fact]
public async Task Deux_issn_qui_se_rejoignent_ne_tuent_pas_le_demarrage()
{
_db.Revues.Add(new Revue { Titre = "Médor", TitreNormalise = "medor", Issn = "2466-6718" });
_db.Revues.Add(new Revue { Titre = "Doublon", TitreNormalise = "doublon", Issn = "24666718" });
await _db.SaveChangesAsync();
await ExecuterAsync();
var issn = _db.Revues.OrderBy(r => r.Id).Select(r => r.Issn).ToList();
// Les deux lignes survivent : mal normalisée vaut infiniment mieux que supprimée.
Assert.Equal(["2466-6718", "24666718"], issn);
}
///
/// Une revue sans ISSN n'entre pas dans le jeu des clés : sans quoi la deuxième
/// bloquerait la première, tous les NULL se confondant en une seule valeur.
///
[Fact]
public async Task Les_revues_sans_issn_ne_se_bloquent_pas_entre_elles()
{
_db.Revues.Add(new Revue { Titre = "Une", TitreNormalise = "une" });
_db.Revues.Add(new Revue { Titre = "Deux", TitreNormalise = "deux" });
_db.Revues.Add(new Revue { Titre = "Trois", TitreNormalise = "trois", Issn = "24666718" });
await _db.SaveChangesAsync();
await ExecuterAsync();
Assert.Equal("2466-6718", _db.Revues.Single(r => r.Titre == "Trois").Issn);
}
public void Dispose()
{
_db.Dispose();
_connexion.Dispose();
GC.SuppressFinalize(this);
}
}