using MaBibli.Api.Services.Isbn; using MaBibli.Shared.Dtos; using MaBibli.Shared.Isbn; using Microsoft.Extensions.Logging.Abstractions; namespace MaBibli.Tests; /// /// Codes-barres de périodiques (préfixe 977) : reconnaissance, dérivation de l'ISSN, /// et lecture de la notice BnF correspondante. /// public class CodePeriodiqueTests { /// /// Le cas remonté en usage réel : ce code renvoyait « aucun résultat », alors qu'il /// n'a jamais pu décrire un livre. /// private const string EanMedor = "9772466671438"; [Theory] [InlineData(EanMedor, true)] [InlineData("9782070612758", false)] // Le Petit Prince : un vrai livre [InlineData("9791234567896", false)] // préfixe 979, livre lui aussi [InlineData("2253004227", false)] // ISBN-10 : trop court pour porter un préfixe [InlineData("9772466671437", false)] // bon préfixe, clé EAN fausse [InlineData(null, false)] [InlineData("", false)] public void Reconnait_les_codes_de_periodique(string? ean, bool attendu) => Assert.Equal(attendu, CodePeriodique.EstCodePeriodique(ean)); /// /// La clé de l'ISSN n'est pas dans le code-barres : elle se recalcule. Vérifié /// contre la BnF le 2026-08-18 — l'ISSN 2466-6718 y désigne bien « Médor ». /// [Fact] public void Deduit_lIssn_du_code_barres() => Assert.Equal("2466-6718", CodePeriodique.IssnDepuis(EanMedor)); [Fact] public void Ne_deduit_rien_dun_code_de_livre() => Assert.Null(CodePeriodique.IssnDepuis("9782070612758")); /// /// Les deux chiffres qui précèdent la clé encodent la parution : deux numéros d'une même /// revue doivent donner le même ISSN, sinon chaque magazine créerait une fiche. /// [Fact] public void Deux_parutions_de_la_meme_revue_donnent_le_meme_Issn() { var issns = new[] { "9772466671438", "9772466671391" } .Select(CodePeriodique.IssnDepuis) .ToList(); Assert.Equal("2466-6718", issns[0]); Assert.Equal(issns[0], issns[1]); } [Fact] public void Notice_BnF_de_periodique_est_lue_et_nettoyee() { var revue = BnfSruParser.ParserPeriodique( Fixture.Lire("bnf-issn-2466-6718.xml"), "2466-6718"); Assert.NotNull(revue); // « Médor (Bruxelles) » : le parenthésé est un qualificatif de désambiguïsation, // pas une partie du nom écrit sur la couverture. Assert.Equal("Médor", revue!.Titre); Assert.Equal("2466-6718", revue.Issn); Assert.Equal("2015", revue.Annee); } [Fact] public void Reponse_sans_notice_ne_produit_pas_de_titre_invente() { var vide = """ 0 """; Assert.Null(BnfSruParser.ParserPeriodique(vide, "2466-6718")); } // ── La cascade ne doit pas partir du tout ──────────────────────────────── /// /// Client BnF simulé : compte les appels ISBN pour prouver qu'aucun n'a lieu. /// private sealed class BnfFake(PeriodiqueDetecte? revue) : IBnfClient { public List IsbnInterroges { get; } = []; public List IssnInterroges { get; } = []; public Task<(IReadOnlyList, string?)> RechercherAsync( string isbn, string? urlCouverture, CancellationToken ct = default) { IsbnInterroges.Add(isbn); return Task.FromResult(((IReadOnlyList)[], (string?)null)); } public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync( string auteur, CancellationToken ct = default) => Task.FromResult((new ResultatBibliographie(), (string?)null)); public Task<(PeriodiqueDetecte?, string?)> RechercherPeriodiqueAsync( string issn, CancellationToken ct = default) { IssnInterroges.Add(issn); return Task.FromResult((revue, (string?)null)); } /// Recherche par titre : couverte par ses propres tests. public Task<(IReadOnlyList, string?)> RechercherParTitreAsync( string titre, string? auteur, CancellationToken ct = default) => Task.FromResult(((IReadOnlyList)[], (string?)null)); } private sealed class OpenLibraryFake : IOpenLibraryClient { public int Appels { get; private set; } public Task<(IReadOnlyList, string?)> RechercherAsync( string isbn, string? urlCouverture, CancellationToken ct = default) { Appels++; return Task.FromResult(((IReadOnlyList)[], (string?)null)); } } /// /// Le point du bug : la cascade s'exécutait en entier pour finir sur « aucun résultat ». /// Elle ne doit désormais pas partir — seul l'ISSN est interrogé. /// [Fact] public async Task Un_code_977_ne_declenche_aucune_recherche_de_livre() { var bnf = new BnfFake(new PeriodiqueDetecte { Issn = "2466-6718", Titre = "Médor" }); var ol = new OpenLibraryFake(); var service = new IsbnLookupService(bnf, ol, NullLogger.Instance); var resultat = await service.RechercherAsync(EanMedor); Assert.NotNull(resultat); Assert.Empty(bnf.IsbnInterroges); Assert.Equal(0, ol.Appels); Assert.Equal(["2466-6718"], bnf.IssnInterroges); Assert.Empty(resultat!.Candidats); Assert.Equal("Médor", resultat.Periodique?.Titre); } /// /// BnF muette : on connaît quand même l'ISSN, puisqu'il se déduit du code-barres. /// Ne rien renvoyer ferait retomber l'écran sur « aucun résultat », le bug d'origine. /// [Fact] public async Task BnF_muette_laisse_quand_meme_lIssn() { var service = new IsbnLookupService( new BnfFake(null), new OpenLibraryFake(), NullLogger.Instance); var resultat = await service.RechercherAsync(EanMedor); Assert.Equal("2466-6718", resultat?.Periodique?.Issn); Assert.Null(resultat?.Periodique?.Titre); } /// Un livre ordinaire n'est jamais pris pour un périodique. [Fact] public async Task Un_ISBN_de_livre_passe_par_la_cascade_normale() { var bnf = new BnfFake(null); var service = new IsbnLookupService( bnf, new OpenLibraryFake(), NullLogger.Instance); var resultat = await service.RechercherAsync("9782070612758"); Assert.Null(resultat?.Periodique); Assert.Empty(bnf.IssnInterroges); Assert.NotEmpty(bnf.IsbnInterroges); } }