Files
mabibli/MaBibli.Tests/CodePeriodiqueTests.cs
T
mathieuandClaude Opus 5 5cf566bc33 Traiter les retours d'usage du 2026-08-18 (2ᵉ série)
Six lots, issus d'IDEES.md. Les décisions actées sont reportées dans
CLAUDE.md, et les entrées traitées retirées d'IDEES.md.

1. Douchette USB, ISSN et enchaînement du scan
   - Champ ISBN focalisé à l'ouverture : c'est tout ce qu'exige une
     douchette USB, qui se présente au système comme un clavier. Remède
     au scan caméra, qui rate sur la webcam d'un PC (optique, pas
     décodage).
   - Un EAN en 977 est un périodique : il porte un ISSN, donc un titre de
     revue. Intercepté AVANT la validation ISBN (c'est un EAN-13
     parfaitement valide), l'ISSN est déduit du code puis nommé via
     bib.issn. Auparavant la cascade s'exécutait en entier pour finir sur
     « aucun résultat ».
   - L'enchaînement après scan existait déjà mais était invisible : une
     étape « Recherche » affiche désormais le code interrogé.

2. ISBN affichés avec des tirets
   Tables extraites du RangeMessage.xml officiel — plusieurs tranches ne
   sont pas celles qu'on suppose. Le francophone est découpé en entier,
   ailleurs on s'arrête au groupe et à la clé : aucune coupure fausse.
   Corrige au passage l'export CSV, où un ISBN nu était lu comme un
   nombre par Excel.

3. Hors-ligne : la liste d'envies
   ListerSouhaitsAsync était le seul point de lecture hors du dispositif
   hors-ligne, d'où le « 404 Not Found » brut à l'écran. Cinquième
   instantané, écritures refusées, plus aucun message HTTP. Même défaut
   corrigé sur la bibliographie.

4. Navigation par onglets
   Catalogue / Auteurs / Prêts / Envies dans MainLayout ; les barres
   d'actions ne portent plus que des actions. Filtres repliés derrière un
   bouton compteur, ligne « format » masquée quand le fonds n'a qu'un
   format. Une seule entrée d'ajout, désactivée hors-ligne — pas masquée.

5. Liste d'envies : ordre, recherche, couvertures
   Migration RangDesEnvies. Le remplissage reconduit l'ordre affiché
   jusqu'ici : sans lui, les listes existantes se seraient réordonnées
   toutes seules. Réordonnancement par flèches et glisser-déposer (le
   drag & drop HTML5 ne marche pas au doigt). Ajout dans son propre
   écran, avec recherche par titre (bib.title) et couvertures enfin
   alimentées.

6. Bibliographie : une source muette n'est pas une liste vide
   L'écran affichait « BnF injoignable » PUIS « la BnF ne connaît aucun
   livre de cet auteur » — la seconde phrase étant fausse. Les deux cas
   s'excluent désormais, et un bouton Réessayer est offert.

   Vérifié en exécution : le diagnostic d'IDEES.md était faux sur un
   point. Robert A. Harper a bien 7 œuvres à la BnF (85 notices
   annoncées) ; c'était le même délai dépassé observé deux fois, pris
   pour deux causes distinctes.

380 tests, dont un qui applique réellement la migration (EnsureCreated
n'en joue aucune) et un qui verrouille les messages atteignant
l'utilisateur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 00:14:01 +02:00

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);
}
}