Files
mabibli/MaBibli.Tests/IsbnLookupServiceTests.cs
T
mathieuandClaude Opus 5 8b8fe3be6e Ajouter la liste d'envies personnelle, son export et la bibliographie par auteur
La liste d'envies vit dans une table séparée (LivreSouhaite) plutôt que dans un
statut de plus sur Livre : un livre souhaité n'est pas possédé, et le loger dans
Livres l'aurait fait entrer dans le catalogue, les compteurs et les prêts, au
prix d'un « et qui n'est pas souhaité » à répéter dans chaque lecture. La portée
est personnelle, comme le statut de lecture — mais ici Utilisateur est une vraie
frontière : toute lecture filtre dessus.

Bibliographie : SRU BnF interrogé par bib.author, vérifié le 2026-08-18. Deux
filtres mesurés sur des réponses réelles sont indispensables — le type de
document (l'index mêle livres audio, jeux et spectacles) et surtout l'auteur
réel de la notice, « all » rapprochant les mots sur l'ensemble des auteurs :
« Émile Zola » remonte sinon toute l'œuvre de sa fille Denise Le Blond-Zola.
Les rééditions sont regroupées par clé d'œuvre (183 notices Werber -> 51
œuvres). Le rapprochement avec l'étagère se fait par titre, pas par ISBN, qui
désigne une édition et non une œuvre ; ses limites sont dites à l'écran.

Export en deux formats, tous deux du texte sans dépendance : .txt groupé par
auteur pour la librairie, .csv à séparateur point-virgule et BOM pour le
tableur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 13:59:06 +02:00

202 lines
7.3 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));
}
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));
}
}