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>
184 lines
7.0 KiB
C#
184 lines
7.0 KiB
C#
using MaBibli.Api.Services.Isbn;
|
|
using MaBibli.Shared.Dtos;
|
|
using MaBibli.Shared.Isbn;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace MaBibli.Tests;
|
|
|
|
/// <summary>
|
|
/// Codes-barres de périodiques (préfixe <c>977</c>) : reconnaissance, dérivation de l'ISSN,
|
|
/// et lecture de la notice BnF correspondante.
|
|
/// </summary>
|
|
public class CodePeriodiqueTests
|
|
{
|
|
/// <summary>
|
|
/// Le cas remonté en usage réel : ce code renvoyait « aucun résultat », alors qu'il
|
|
/// n'a jamais pu décrire un livre.
|
|
/// </summary>
|
|
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));
|
|
|
|
/// <summary>
|
|
/// La clé de l'ISSN n'est <b>pas</b> 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 ».
|
|
/// </summary>
|
|
[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"));
|
|
|
|
/// <summary>
|
|
/// Les deux chiffres qui précèdent la clé encodent la parution : deux numéros d'une même
|
|
/// revue doivent donner le <b>même</b> ISSN, sinon chaque magazine créerait une fiche.
|
|
/// </summary>
|
|
[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 = """
|
|
<srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/">
|
|
<srw:numberOfRecords>0</srw:numberOfRecords>
|
|
</srw:searchRetrieveResponse>
|
|
""";
|
|
|
|
Assert.Null(BnfSruParser.ParserPeriodique(vide, "2466-6718"));
|
|
}
|
|
|
|
// ── La cascade ne doit pas partir du tout ────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Client BnF simulé : compte les appels ISBN pour prouver qu'aucun n'a lieu.
|
|
/// </summary>
|
|
private sealed class BnfFake(PeriodiqueDetecte? revue) : IBnfClient
|
|
{
|
|
public List<string> IsbnInterroges { get; } = [];
|
|
|
|
public List<string> IssnInterroges { get; } = [];
|
|
|
|
public Task<(IReadOnlyList<CandidatLivre>, string?)> RechercherAsync(
|
|
string isbn, string? urlCouverture, CancellationToken ct = default)
|
|
{
|
|
IsbnInterroges.Add(isbn);
|
|
return Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (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));
|
|
}
|
|
|
|
/// <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 int Appels { get; private set; }
|
|
|
|
public Task<(IReadOnlyList<CandidatLivre>, string?)> RechercherAsync(
|
|
string isbn, string? urlCouverture, CancellationToken ct = default)
|
|
{
|
|
Appels++;
|
|
return Task.FromResult(((IReadOnlyList<CandidatLivre>)[], (string?)null));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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é.
|
|
/// </summary>
|
|
[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<IsbnLookupService>.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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task BnF_muette_laisse_quand_meme_lIssn()
|
|
{
|
|
var service = new IsbnLookupService(
|
|
new BnfFake(null), new OpenLibraryFake(), NullLogger<IsbnLookupService>.Instance);
|
|
|
|
var resultat = await service.RechercherAsync(EanMedor);
|
|
|
|
Assert.Equal("2466-6718", resultat?.Periodique?.Issn);
|
|
Assert.Null(resultat?.Periodique?.Titre);
|
|
}
|
|
|
|
/// <summary>Un livre ordinaire n'est jamais pris pour un périodique.</summary>
|
|
[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<IsbnLookupService>.Instance);
|
|
|
|
var resultat = await service.RechercherAsync("9782070612758");
|
|
|
|
Assert.Null(resultat?.Periodique);
|
|
Assert.Empty(bnf.IssnInterroges);
|
|
Assert.NotEmpty(bnf.IsbnInterroges);
|
|
}
|
|
}
|