using MaBibli.Api.Data; using MaBibli.Api.Services.Revues; using MaBibli.Shared.Dtos; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace MaBibli.Tests; /// /// Revues et numéros, sur une base SQLite en mémoire : l'unicité d'un ISSN passe par un /// index partiel, qui n'existe qu'en base. /// /// /// L'invariant central : une fiche par revue, les numéros à l'intérieur. C'est ce qui /// évite douze fiches identiques pour douze numéros d'un même magazine. /// public class ServiceRevuesTests : IDisposable { private readonly SqliteConnection _connexion; private readonly MaBibliDbContext _db; private readonly ServiceRevues _revues; private const string Mathieu = "mathieu"; public ServiceRevuesTests() { _connexion = new SqliteConnection("Data Source=:memory:"); _connexion.Open(); var options = new DbContextOptionsBuilder() .UseSqlite(_connexion) .Options; _db = new MaBibliDbContext(options); _db.Database.EnsureCreated(); _revues = new ServiceRevues(_db); } private async Task RevueAsync(string titre = "Médor", string? issn = "2466-6718") { var resultat = await _revues.CreerOuRetrouverAsync( new EnregistrementRevue { Titre = titre, Issn = issn }, Mathieu); Assert.Null(resultat.Erreur); return resultat.Revue!; } [Fact] public async Task Rescanner_la_meme_revue_ne_cree_pas_une_seconde_fiche() { var premiere = await RevueAsync(); // Le second numéro du même magazine porte le même ISSN : c'est le cas NORMAL du flux // « 977 », et il ne doit surtout pas produire une fiche de plus. var seconde = await RevueAsync("Médor (Bruxelles)"); Assert.Equal(premiere.Id, seconde.Id); Assert.Single(await _revues.ListerAsync()); } [Fact] public async Task Une_revue_saisie_a_la_main_apprend_son_issn_au_premier_scan() { var aLaMain = await RevueAsync("Médor", issn: null); var scannee = await RevueAsync("Médor"); Assert.Equal(aLaMain.Id, scannee.Id); Assert.Equal("2466-6718", scannee.Issn); } [Fact] public async Task Les_numeros_vivent_dans_la_revue_et_ne_font_pas_de_fiches() { var revue = await RevueAsync(); await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43", DateParution = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var apres = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "44", DateParution = new DateTime(2024, 9, 1, 0, 0, 0, DateTimeKind.Utc), })).Revue!; Assert.Single(await _revues.ListerAsync()); // Du plus récent au plus ancien : sur un magazine, c'est le dernier reçu qu'on cherche. Assert.Equal(["44", "43"], apres.Numeros.Select(n => n.Numero)); } [Fact] public async Task Un_numero_sans_numero_est_refuse() { var revue = await RevueAsync(); var resultat = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = " " }); Assert.NotNull(resultat.Erreur); } [Fact] public async Task Le_meme_numero_ne_sajoute_pas_deux_fois() { var revue = await RevueAsync(); await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43" }); var doublon = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43" }); Assert.NotNull(doublon.Erreur); } [Fact] public async Task Un_numero_a_paraitre_est_refuse() { var revue = await RevueAsync(); var resultat = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "99", DateParution = DateTime.UtcNow.AddMonths(2), }); Assert.NotNull(resultat.Erreur); } [Fact] public async Task Les_numeros_sans_date_ferment_la_liste() { var revue = await RevueAsync(); await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "hors-série 1" }); var apres = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43", DateParution = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc), })).Revue!; // Une date inconnue ne doit pas faire passer un numéro pour le plus ancien. Assert.Equal(["43", "hors-série 1"], apres.Numeros.Select(n => n.Numero)); } [Fact] public async Task Supprimer_la_revue_emporte_ses_numeros() { var revue = await RevueAsync(); await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43" }); Assert.True(await _revues.SupprimerAsync(revue.Id)); // Contrairement aux livres d'une série, un numéro n'existe que par sa revue. Assert.Empty(await _db.NumerosRevue.ToListAsync()); } [Fact] public async Task Deux_revues_ne_partagent_ni_titre_ni_issn() { await RevueAsync(); var autre = await RevueAsync("Socialter", issn: "2270-4426"); var titrePris = await _revues.ModifierAsync( autre.Id, new EnregistrementRevue { Titre = "médor" }); var issnPris = await _revues.ModifierAsync( autre.Id, new EnregistrementRevue { Titre = "Socialter", Issn = "2466-6718" }); Assert.NotNull(titrePris.Erreur); Assert.NotNull(issnPris.Erreur); } /// /// ⚠️ Le cas qui a motivé la canonisation (lot K2) : une revue saisie à la main, ISSN tapé /// sans tiret, doit être retrouvée par le scan — qui produit toujours la forme à /// tiret. Sans elle, le code-barres créait une seconde fiche à côté de la première. /// [Fact] public async Task Un_issn_sans_tiret_designe_la_meme_revue_que_le_code_barres() { var aLaMain = await RevueAsync("Médor", issn: "24666718"); Assert.Equal("2466-6718", aLaMain.Issn); var scanne = await RevueAsync("Médor (Bruxelles)", issn: "2466-6718"); Assert.Equal(aLaMain.Id, scanne.Id); Assert.Single(await _revues.ListerAsync()); } [Fact] public async Task Un_numero_porte_sa_couverture_et_ses_articles_a_la_une() { var revue = await RevueAsync(); var apres = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43", CoverUrl = "https://exemple.fr/medor-43.jpg", Articles = ["Ukraine, deux ans après", "Le retour du train de nuit"], }); var numero = Assert.Single(apres.Revue!.Numeros); Assert.Equal("https://exemple.fr/medor-43.jpg", numero.CoverUrl); Assert.Equal(["Ukraine, deux ans après", "Le retour du train de nuit"], numero.Articles); } /// /// La une se recopie après coup : on note un numéro le jour où on le range, on en /// recopie le sommaire plus tard. Sans modification, ces champs n'auraient existé qu'à la /// création, c'est-à-dire jamais. /// [Fact] public async Task Un_numero_apprend_sa_une_apres_coup() { var revue = await RevueAsync(); var cree = (await _revues.AjouterNumeroAsync( revue.Id, new AjoutNumeroRevue { Numero = "43" })).Revue!; var numeroId = cree.Numeros[0].Id; var apres = await _revues.ModifierNumeroAsync(numeroId, new AjoutNumeroRevue { Numero = "43", Articles = ["Le retour du train de nuit"], }); Assert.Equal(["Le retour du train de nuit"], apres.Revue!.Numeros[0].Articles); } /// /// ⚠️ Remplacement et non fusion : la ligne de saisie est la liste. Un titre effacé /// du champ doit disparaître, comme pour les thèmes d'un livre. /// [Fact] public async Task Reecrire_la_une_remplace_lancienne() { var revue = await RevueAsync(); var cree = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43", Articles = ["Premier", "Second"], })).Revue!; var apres = await _revues.ModifierNumeroAsync( cree.Numeros[0].Id, new AjoutNumeroRevue { Numero = "43", Articles = ["Troisième"] }); Assert.Equal(["Troisième"], apres.Revue!.Numeros[0].Articles); Assert.Single(await _db.ArticlesUne.ToListAsync()); } [Fact] public async Task Retirer_le_numero_emporte_sa_une() { var revue = await RevueAsync(); var cree = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43", Articles = ["Ukraine, deux ans après"], })).Revue!; Assert.True(await _revues.RetirerNumeroAsync(cree.Numeros[0].Id)); // Cascade : un article à la une n'existe que par la parution qui le porte. Assert.Empty(await _db.ArticlesUne.ToListAsync()); } /// /// L'unicité (NumeroRevueId, TitreNormalise) vit en base : un doublon envoyé /// directement à l'API ferait échouer l'écriture si le service ne l'écartait pas. /// [Fact] public async Task Le_meme_article_ne_sannonce_pas_deux_fois() { var revue = await RevueAsync(); var apres = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43", Articles = ["Ukraine, deux ans après", "ukraine, DEUX ans après", " "], }); Assert.Equal(["Ukraine, deux ans après"], apres.Revue!.Numeros[0].Articles); } [Fact] public async Task Deux_numeros_peuvent_titrer_pareil() { var revue = await RevueAsync(); await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43", Articles = ["Le dossier de l'année"], }); var apres = await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "44", Articles = ["Le dossier de l'année"], }); // L'unicité est bornée au numéro : deux parutions peuvent parfaitement titrer pareil. Assert.Null(apres.Erreur); Assert.Equal(2, (await _db.ArticlesUne.ToListAsync()).Count); } /// /// L'écran d'édition (lot W) affiche telle quelle la revue rendue par la /// modification d'un numéro : elle doit donc revenir déjà rangée. /// /// /// ⚠️ C'est ce qui empêche de croire qu'on a mal saisi une date : corriger la parution d'un /// numéro le déplace dans la liste, et l'écran ne recharge rien de son côté. Les numéros /// sans date ferment la liste, ici comme partout — une date inconnue n'est pas une date /// ancienne. /// [Fact] public async Task Corriger_la_parution_range_le_numero_a_sa_place() { var revue = await RevueAsync(); await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43", DateParution = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc), }); var cree = (await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "hors-série 1", })).Revue!; Assert.Equal(["43", "hors-série 1"], cree.Numeros.Select(n => n.Numero)); var horsSerie = cree.Numeros.First(n => n.Numero == "hors-série 1").Id; var apres = await _revues.ModifierNumeroAsync(horsSerie, new AjoutNumeroRevue { Numero = "hors-série 1", DateParution = new DateTime(2025, 3, 1, 0, 0, 0, DateTimeKind.Utc), }); Assert.Equal(["hors-série 1", "43"], apres.Revue!.Numeros.Select(n => n.Numero)); } /// /// Retirer un numéro depuis l'écran d'édition ne touche qu'à lui : les autres parutions et /// la fiche de la revue restent en place. /// [Fact] public async Task Retirer_un_numero_laisse_la_revue_et_ses_autres_numeros() { var revue = await RevueAsync(); await _revues.AjouterNumeroAsync(revue.Id, new AjoutNumeroRevue { Numero = "43" }); var cree = (await _revues.AjouterNumeroAsync( revue.Id, new AjoutNumeroRevue { Numero = "44" })).Revue!; Assert.True(await _revues.RetirerNumeroAsync(cree.Numeros.First(n => n.Numero == "44").Id)); var restantes = await _revues.ListerAsync(); Assert.Equal(["43"], Assert.Single(restantes).Numeros.Select(n => n.Numero)); } public void Dispose() { _db.Dispose(); _connexion.Dispose(); GC.SuppressFinalize(this); } }