Files
mabibli/MaBibli.Tests/BnfSruParserTests.cs
T
mathieuandClaude Opus 5 b594a4b2c1 Sortir le statut de lecture et l'auteur de la table Livre
Deux changements de modele en une seule migration, plus la recherche qui
en depend.

Le statut de lecture devient personnel. Il etait une colonne de Livre,
donc partage par tout le foyer, alors que deux membres lisent le meme
exemplaire a des rythmes differents. Il vit desormais dans une table
(LivreId, Utilisateur, Statut) avec unicite sur le couple. L'absence de
ligne vaut « non commence » : sur une bibliotheque de foyer la plupart
des couples n'ont aucun statut, et les materialiser tous multiplierait
les lignes par le nombre de comptes pour n'exprimer qu'un vide. Rien
n'est donc ecrit a la creation d'un livre.

Piege rencontre : un Dictionary<int, Statut> renvoyait la valeur 0 de
l'enumeration — « À lire » — pour un livre sans ligne, rendant « non
commence » indiscernable d'un choix explicite. Le dictionnaire est
desormais typé Statut?.

L'auteur devient une table. Deux formes normalisees y cohabitent, et ce
n'est pas une redondance : NomNormalise garde l'ordre de saisie pour la
recherche en sous-chaine, CleRegroupement trie les mots et porte l'index
unique, donc l'invariant « un auteur, une fiche ». Les initiales
echappent a la cle et sont traitees en memoire, sur une table qui compte
au plus quelques centaines de lignes.

Un livre peut avoir plusieurs auteurs — le lookup ISBN en renvoie quatre
pour Introduction to Algorithms — d'ou la table de liaison, avec une
position qui conserve l'ordre de la couverture.

Les rapprochements ambigus ne sont jamais appliques seuls : l'API les
liste, l'utilisateur accepte ou refuse, et les refus sont memorises pour
que la suggestion ne revienne pas. Le couple refuse est range par
identifiant croissant, donc un refus vaut dans les deux sens.

Reprise des donnees existantes. L'ancien statut, commun, est rattache a
AjoutePar — seule personne que la base associe au livre. Les statuts des
livres sans AjoutePar sont perdus : les attribuer serait une invention.
Les trois valeurs sont reprises telles quelles, « À lire » compris, parce
que c'est ce que l'ancienne interface affichait. L'ancien champ auteur
devient une fiche par valeur distincte.

La migration ne peut pas tout faire : lower() de SQLite ne retire pas les
accents, donc « Émile Zola » et « emile zola » y restent deux fiches.
ServiceRenormalisation finit le travail en C# au demarrage, reunit ces
variantes, applique aussi la regle des initiales — sans quoi une base
heritee resterait eclatee la ou une saisie neuve aurait ete reunie
d'emblee — et garde le nom d'affichage le plus presentable. Il est
idempotent, et sert de filet si les regles de normalisation changent.

L'ordre de la migration compte : les colonnes condamnees sont recopiees
dans une table de transit avant d'etre supprimees, parce que supprimer
une colonne sous SQLite reconstruit la table.

Verifie sur une base a l'ancien schema contenant 9 livres, 2 prets et
trois variantes de Zola : prets intacts, statuts rattaches, les trois
Zola reunis sous « Émile Zola », « P.F. Hamilton » absorbe par
« Peter F. Hamilton », « Hamilton » seul laisse en suggestion.

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

160 lines
6.6 KiB
C#

using MaBibli.Api.Services.Isbn;
using MaBibli.Shared.Dtos;
namespace MaBibli.Tests;
/// <summary>
/// Parsing de réponses SRU réelles de la BnF (schéma <c>dublincore</c>).
/// </summary>
public class BnfSruParserTests
{
[Theory]
// Le piège bloquant, mesuré : ces trois ISBN-13 donnent 0 notice, leur ISBN-10 en donne.
[InlineData("bnf-9782070612758.xml", 1)] // Le Petit Prince (2007) : trouvé en ISBN-13
[InlineData("bnf-9782253004226.xml", 0)] // Germinal en ISBN-13 : rien
[InlineData("bnf-2253004227.xml", 3)] // Germinal en ISBN-10 : 3 rééditions
[InlineData("bnf-9782080704092.xml", 0)] // Le Horla en ISBN-13 : rien
[InlineData("bnf-2080704095.xml", 1)] // Le Horla en ISBN-10 : 1 notice
[InlineData("bnf-9780262033848.xml", 0)] // Livre étranger : absent du dépôt légal français
[InlineData("bnf-0262033844.xml", 0)] // …sous les deux formes d'ISBN
public void NombreDeNotices_correspond_aux_mesures(string fixture, int attendu)
=> Assert.Equal(attendu, BnfSruParser.LireNombreDeNotices(Fixture.Lire(fixture)));
[Fact]
public void Petit_Prince_est_parse_et_nettoye()
{
var candidats = BnfSruParser.Parser(
Fixture.Lire("bnf-9782070612758.xml"), "9782070612758", "https://couverture");
var c = Assert.Single(candidats);
Assert.Equal("Le petit prince", c.Titre);
Assert.Equal("Antoine de Saint-Exupéry", c.Auteur);
Assert.Equal("Gallimard", c.Editeur);
Assert.Equal("2007", c.Annee);
Assert.Equal("fre", c.Langue);
Assert.Equal(SourceMetadonnees.Bnf, c.Source);
Assert.Equal("9782070612758", c.IsbnInterroge);
Assert.Equal("https://couverture", c.CoverUrl);
Assert.StartsWith("ark:/12148/", c.IdentifiantSource);
}
[Fact]
public void Germinal_renvoie_les_trois_reeditions_distinctes()
{
// Décision actée : on ne choisit pas à la place de l'utilisateur, on remonte tout.
var candidats = BnfSruParser.Parser(
Fixture.Lire("bnf-2253004227.xml"), "2253004227", null);
Assert.Equal(3, candidats.Count);
Assert.All(candidats, c => Assert.Equal("Germinal", c.Titre));
Assert.All(candidats, c => Assert.Equal("Émile Zola", c.Auteur));
Assert.Equal(
["le Livre de poche", "Librairie générale française", "Librairie générale française"],
candidats.Select(c => c.Editeur));
Assert.Equal(["1979", "2000", "1983"], candidats.Select(c => c.Annee));
// Les notices doivent rester discernables : sans ark distinct l'utilisateur ne peut pas trancher.
Assert.Equal(3, candidats.Select(c => c.IdentifiantSource).Distinct().Count());
}
[Fact]
public void Le_Horla_est_parse_depuis_lisbn10()
{
var candidats = BnfSruParser.Parser(
Fixture.Lire("bnf-2080704095.xml"), "2080704095", null);
var c = Assert.Single(candidats);
// La BnF écrit « Le horla » : le nettoyage ne doit pas « corriger » la casse de la source.
Assert.Equal("Le horla", c.Titre);
Assert.Equal("Guy de Maupassant", c.Auteur);
Assert.Equal("2080704095", c.IsbnInterroge);
Assert.DoesNotContain("(", c.Editeur ?? string.Empty);
}
/// <summary>
/// Notice minimale bâtie à la main, au format de celles de la BnF.
/// </summary>
/// <remarks>
/// Les cas ci-dessous n'ont pas de réponse réelle enregistrée : ils portent sur la manière
/// dont le parseur <b>combine</b> ses règles, pas sur la lecture du XML — que les fixtures
/// réelles couvrent déjà.
/// </remarks>
private static string Notice(string titre, params string[] auteurs) =>
$"""
<srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/">
<srw:numberOfRecords>1</srw:numberOfRecords>
<srw:records><srw:record>
<srw:recordData xmlns:dc="http://purl.org/dc/elements/1.1/">
<dc:title>{titre}</dc:title>
{string.Concat(auteurs.Select(a => $"<dc:creator>{a}</dc:creator>"))}
</srw:recordData>
</srw:record></srw:records>
</srw:searchRetrieveResponse>
""";
/// <summary>
/// Cas réel de l'ISBN <c>9782749942636</c> : la BnF recolle le nom de l'auteur en fin de
/// titre <b>sans</b> le séparateur ISBD. Il faut donc les auteurs nettoyés avant de pouvoir
/// finir le titre — c'est l'ordre des opérations qui est vérifié ici.
/// </summary>
[Fact]
public void Le_nom_de_lauteur_colle_en_fin_de_titre_est_retire()
{
var candidats = BnfSruParser.Parser(
Notice(
"Le plus grand défi de l'histoire de l'humanité : face à la catastrophe "
+ "écologique et sociale (Nouvelle éd. augmentée) Aurélien Barrau",
"Barrau, Aurélien (1973-....). Auteur du texte"),
"9782749942636",
null);
var c = Assert.Single(candidats);
Assert.Equal(
"Le plus grand défi de l'histoire de l'humanité : face à la catastrophe écologique "
+ "et sociale (Nouvelle éd. augmentée)",
c.Titre);
Assert.Equal(["Aurélien Barrau"], c.Auteurs);
}
/// <summary>Contre-exemple : un nom de personne ailleurs que tout à la fin reste en place.</summary>
[Fact]
public void Un_nom_de_personne_dans_le_titre_nest_pas_mutile()
{
var candidats = BnfSruParser.Parser(
Notice("Lettre à Émile Zola et autres textes", "Maupassant, Guy de (1850-1893)"),
"0000000000",
null);
Assert.Equal("Lettre à Émile Zola et autres textes", Assert.Single(candidats).Titre);
}
[Fact]
public void Plusieurs_dc_creator_donnent_plusieurs_auteurs()
{
var candidats = BnfSruParser.Parser(
Notice(
"Le Horla et autres nouvelles",
"Maupassant, Guy de (1850-1893). Auteur du texte",
"Zola, Émile (1840-1902). Préfacier"),
"0000000000",
null);
var c = Assert.Single(candidats);
Assert.Equal(["Guy de Maupassant", "Émile Zola"], c.Auteurs);
// La forme « une ligne » reste disponible pour un champ de saisie unique.
Assert.Equal("Guy de Maupassant; Émile Zola", c.Auteur);
}
[Fact]
public void Reponse_sans_notice_donne_une_liste_vide_et_pas_une_exception()
{
var candidats = BnfSruParser.Parser(
Fixture.Lire("bnf-9780262033848.xml"), "9780262033848", null);
Assert.Empty(candidats);
}
}