Files
mabibli/MaBibli.Tests/IsbnLookupServiceTests.cs
Mathieu LimonierandClaude Opus 5 6a6d745af4 MaBibli 1.0.0
Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts,
scan de code-barres, consultation hors-ligne.

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

228 lines
8.8 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));
}
/// <summary>
/// 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.
/// </summary>
public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync(
string auteur, CancellationToken ct = default) =>
Task.FromResult((new ResultatBibliographie(), (string?)null));
/// <summary>
/// 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.
/// </summary>
public Task<(PeriodiqueDetecte?, string?)> RechercherPeriodiqueAsync(
string issn, CancellationToken ct = default) =>
Task.FromResult(((PeriodiqueDetecte?)null, (string?)null));
/// <summary>Recherche par titre : couverte par ses propres tests.</summary>
public Task<(IReadOnlyList<CandidatLivre>, string?)> RechercherParTitreAsync(
string titre, string? auteur, CancellationToken ct = default) =>
Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (string?)null));
}
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));
}
/// <summary>
/// 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.
/// </summary>
public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync(
string auteur, CancellationToken ct = default) =>
Task.FromResult((new ResultatBibliographie(), (string?)null));
/// <summary>
/// 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.
/// </summary>
public Task<(PeriodiqueDetecte?, string?)> RechercherPeriodiqueAsync(
string issn, CancellationToken ct = default) =>
Task.FromResult(((PeriodiqueDetecte?)null, (string?)null));
/// <summary>Recherche par titre : couverte par ses propres tests.</summary>
public Task<(IReadOnlyList<CandidatLivre>, string?)> RechercherParTitreAsync(
string titre, string? auteur, CancellationToken ct = default) =>
Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (string?)null));
}
}