Files
mabibli/MaBibli.Tests/IsbnLookupServiceTests.cs
T
mathieuandClaude Opus 5 f8f30dbcf7 Phase 2 : service de lookup ISBN en cascade BnF puis OpenLibrary
Recuperation des metadonnees d'un livre a partir de son ISBN, cote serveur
uniquement (aucune interface, aucune persistance).

Cascade : BnF en ISBN-13, puis BnF en ISBN-10 converti, puis OpenLibrary.
La conversion 13 -> 10 est indispensable et non optionnelle : la BnF indexe
l'ISBN tel qu'imprime, les ouvrages d'avant 2007 ne portent qu'un ISBN-10 et
sont introuvables par l'EAN-13 que lit le scanner.

Toutes les notices trouvees sont remontees (jusqu'a 5) : un meme ISBN peut
correspondre a plusieurs reeditions, le choix revient a l'utilisateur.

Le double appel /isbn puis /authors d'OpenLibrary est bien fait, avec repli
sur l'oeuvre quand l'edition ne porte aucun auteur : c'est le bug de BookLogr
(titre rempli, auteur vide) qu'il ne faut pas reproduire.

La couverture vient toujours d'OpenLibrary, avec ?default=false pour obtenir
un 404 plutot qu'une image placeholder.

87 tests xUnit, sur fixtures enregistrees : aucune dependance au reseau.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:22:47 +02:00

186 lines
6.5 KiB
C#

using MaBibli.Api.Services.Isbn;
using MaBibli.Shared.Dtos;
using Microsoft.Extensions.Logging.Abstractions;
namespace MaBibli.Tests;
/// <summary>
/// Enchaînement de la cascade BnF (ISBN-13) → BnF (ISBN-10) → OpenLibrary,
/// avec des sources simulées : aucun accès réseau.
/// </summary>
public class IsbnLookupServiceTests
{
private sealed class BnfFake : IBnfClient
{
public required Func<string, IReadOnlyList<CandidatLivre>> Reponse { get; init; }
public List<string> IsbnInterroges { get; } = [];
public string? Avertissement { get; init; }
public Task<(IReadOnlyList<CandidatLivre>, string?)> RechercherAsync(
string isbn, string? urlCouverture, CancellationToken ct = default)
{
IsbnInterroges.Add(isbn);
return Task.FromResult((Reponse(isbn), Avertissement));
}
}
private sealed class OpenLibraryFake : IOpenLibraryClient
{
public required Func<string, IReadOnlyList<CandidatLivre>> Reponse { get; init; }
public List<string> IsbnInterroges { get; } = [];
public Task<(IReadOnlyList<CandidatLivre>, 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<IsbnLookupService>.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<CandidatLivre>, string?)> RechercherAsync(
string isbn, string? urlCouverture, CancellationToken ct = default)
{
DerniereCouverture = urlCouverture;
return Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (string?)null));
}
}
}