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