using MaBibli.Api.Services.Isbn; using MaBibli.Shared.Dtos; using Microsoft.Extensions.Logging.Abstractions; namespace MaBibli.Tests; /// /// Enchaînement de la cascade BnF (ISBN-13) → BnF (ISBN-10) → OpenLibrary, /// avec des sources simulées : aucun accès réseau. /// public class IsbnLookupServiceTests { private sealed class BnfFake : IBnfClient { public required Func> Reponse { get; init; } public List IsbnInterroges { get; } = []; public string? Avertissement { get; init; } public Task<(IReadOnlyList, string?)> RechercherAsync( string isbn, string? urlCouverture, CancellationToken ct = default) { IsbnInterroges.Add(isbn); return Task.FromResult((Reponse(isbn), Avertissement)); } /// /// Ces tests ne portent que sur la cascade ISBN : la recherche par auteur n'y intervient /// pas, mais l'interface l'exige. Elle est couverte par BnfBibliographieParserTests. /// public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync( string auteur, CancellationToken ct = default) => Task.FromResult((new ResultatBibliographie(), (string?)null)); /// /// Ces tests ne portent que sur la cascade ISBN d'un livre : le préfixe 977 (périodique) /// est couvert par ses propres tests. L'interface l'exige, d'où ce stub. /// public Task<(PeriodiqueDetecte?, string?)> RechercherPeriodiqueAsync( string issn, CancellationToken ct = default) => Task.FromResult(((PeriodiqueDetecte?)null, (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 required Func> Reponse { get; init; } public List IsbnInterroges { get; } = []; public Task<(IReadOnlyList, string?)> RechercherAsync( string isbn, string? urlCouverture, CancellationToken ct = default) { IsbnInterroges.Add(isbn); return Task.FromResult((Reponse(isbn), (string?)null)); } } private static CandidatLivre Notice(SourceMetadonnees source, string isbn) => new() { Titre = "Un titre", Source = source, IsbnInterroge = isbn }; private static IsbnLookupService Service(IBnfClient bnf, IOpenLibraryClient ol) => new(bnf, ol, NullLogger.Instance); [Fact] public async Task Sarrete_a_la_BnF_quand_lISBN13_suffit() { var bnf = new BnfFake { Reponse = isbn => [Notice(SourceMetadonnees.Bnf, isbn)] }; var ol = new OpenLibraryFake { Reponse = _ => [] }; var r = await Service(bnf, ol).RechercherAsync("978-2-07-061275-8"); Assert.NotNull(r); Assert.Equal("9782070612758", r.IsbnDemande); Assert.Equal("2070612759", r.Isbn10); Assert.Equal(["9782070612758"], bnf.IsbnInterroges); Assert.Empty(ol.IsbnInterroges); Assert.Single(r.Candidats); } [Fact] public async Task Reessaie_la_BnF_avec_lISBN10_converti() { // Le cas Germinal : rien en ISBN-13, tout en ISBN-10. var bnf = new BnfFake { Reponse = isbn => isbn == "2253004227" ? [Notice(SourceMetadonnees.Bnf, isbn), Notice(SourceMetadonnees.Bnf, isbn)] : [], }; var ol = new OpenLibraryFake { Reponse = _ => [] }; var r = await Service(bnf, ol).RechercherAsync("9782253004226"); Assert.Equal(["9782253004226", "2253004227"], bnf.IsbnInterroges); Assert.Empty(ol.IsbnInterroges); Assert.Equal(2, r!.Candidats.Count); Assert.All(r.Candidats, c => Assert.Equal("2253004227", c.IsbnInterroge)); } [Fact] public async Task Bascule_sur_OpenLibrary_quand_la_BnF_ignore_les_deux_formes() { var bnf = new BnfFake { Reponse = _ => [] }; var ol = new OpenLibraryFake { Reponse = isbn => [Notice(SourceMetadonnees.OpenLibrary, isbn)], }; var r = await Service(bnf, ol).RechercherAsync("9780262033848"); Assert.Equal(["9780262033848", "0262033844"], bnf.IsbnInterroges); // OpenLibrary est interrogée avec l'ISBN-13, la forme qu'elle indexe. Assert.Equal(["9780262033848"], ol.IsbnInterroges); Assert.Equal(SourceMetadonnees.OpenLibrary, Assert.Single(r!.Candidats).Source); } [Fact] public async Task Une_BnF_en_panne_ne_fait_pas_echouer_la_cascade() { var bnf = new BnfFake { Reponse = _ => [], Avertissement = "BnF injoignable" }; var ol = new OpenLibraryFake { Reponse = isbn => [Notice(SourceMetadonnees.OpenLibrary, isbn)] }; var r = await Service(bnf, ol).RechercherAsync("9782253004226"); Assert.Single(r!.Candidats); Assert.Equal(2, r.Avertissements.Count); // une par tentative BnF } [Fact] public async Task Aucune_source_ne_trouve_rien() { var bnf = new BnfFake { Reponse = _ => [] }; var ol = new OpenLibraryFake { Reponse = _ => [] }; var r = await Service(bnf, ol).RechercherAsync("9782253004226"); Assert.NotNull(r); Assert.Empty(r.Candidats); Assert.Empty(r.Avertissements); } [Fact] public async Task ISBN_en_979_ninterroge_la_BnF_quune_seule_fois() { var bnf = new BnfFake { Reponse = _ => [] }; var ol = new OpenLibraryFake { Reponse = _ => [] }; var r = await Service(bnf, ol).RechercherAsync("9791023507027"); Assert.Null(r!.Isbn10); Assert.Equal(["9791023507027"], bnf.IsbnInterroges); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData("pas-un-isbn")] [InlineData("9782253004225")] // clé fausse [InlineData("123")] public async Task ISBN_invalide_renvoie_null(string entree) { var bnf = new BnfFake { Reponse = _ => [] }; var ol = new OpenLibraryFake { Reponse = _ => [] }; Assert.Null(await Service(bnf, ol).RechercherAsync(entree)); Assert.Empty(bnf.IsbnInterroges); Assert.Empty(ol.IsbnInterroges); } [Fact] public async Task La_couverture_OpenLibrary_est_transmise_meme_pour_une_notice_BnF() { string? couvertureVue = null; var bnf = new BnfFake { Reponse = _ => [] }; var ol = new OpenLibraryFake { Reponse = _ => [] }; // On capture l'URL passée à la BnF via un client dédié. var bnfCapture = new CaptureCouverture(); var r = await Service(bnfCapture, ol).RechercherAsync("9782253004226"); couvertureVue = bnfCapture.DerniereCouverture; Assert.NotNull(r); Assert.Equal( "https://covers.openlibrary.org/b/isbn/9782253004226-L.jpg?default=false", couvertureVue); Assert.Empty(bnf.IsbnInterroges); } private sealed class CaptureCouverture : IBnfClient { public string? DerniereCouverture { get; private set; } public Task<(IReadOnlyList, string?)> RechercherAsync( string isbn, string? urlCouverture, CancellationToken ct = default) { DerniereCouverture = urlCouverture; return Task.FromResult(((IReadOnlyList)[], (string?)null)); } /// /// Ces tests ne portent que sur la cascade ISBN : la recherche par auteur n'y intervient /// pas, mais l'interface l'exige. Elle est couverte par BnfBibliographieParserTests. /// public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync( string auteur, CancellationToken ct = default) => Task.FromResult((new ResultatBibliographie(), (string?)null)); /// /// Ces tests ne portent que sur la cascade ISBN d'un livre : le préfixe 977 (périodique) /// est couvert par ses propres tests. L'interface l'exige, d'où ce stub. /// public Task<(PeriodiqueDetecte?, string?)> RechercherPeriodiqueAsync( string issn, CancellationToken ct = default) => Task.FromResult(((PeriodiqueDetecte?)null, (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)); } }