Files
mabibli/MaBibli.Tests/BnfBibliographieParserTests.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

176 lines
6.7 KiB
C#

using MaBibli.Api.Services.Catalogue;
using MaBibli.Api.Services.Isbn;
namespace MaBibli.Tests;
/// <summary>
/// Lecture des réponses SRU « par auteur », sur des réponses <b>réelles</b> enregistrées le
/// 2026-08-18 (25 notices chacune). Aucun accès réseau.
/// </summary>
/// <remarks>
/// Les deux fixtures sont choisies pour ce qu'elles ont de gênant :
/// <list type="bullet">
/// <item><c>werber</c> : bibliographie propre mais très redondante (rééditions en série) ;</item>
/// <item><c>zola</c> : bibliographie <b>polluée</b> — l'index auteur de la BnF y mêle les
/// ouvrages de sa fille Denise Le Blond-Zola, qui parlent de lui sans qu'il les ait écrits.</item>
/// </list>
/// </remarks>
public class BnfBibliographieParserTests
{
private const string Werber = "bnf-auteur-werber.xml";
private const string Zola = "bnf-auteur-zola.xml";
[Fact]
public void Lit_le_nombre_de_notices_annonce_par_la_BnF()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
// La BnF en annonce bien plus qu'elle n'en renvoie : c'est ce qui permet à l'écran de
// dire honnêtement qu'il ne montre qu'un extrait.
Assert.True(
resultat.NombreAnnonce > resultat.NombreLu,
$"annoncé {resultat.NombreAnnonce}, lu {resultat.NombreLu}");
}
[Fact]
public void Retient_les_notices_de_l_auteur_demande()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
Assert.NotEmpty(resultat.Notices);
Assert.All(
resultat.Notices,
n => Assert.Contains(n.Auteurs, a => a.Contains("Werber", StringComparison.OrdinalIgnoreCase)));
}
/// <summary>
/// Le filtre critique : <c>bib.author all "Émile Zola"</c> rapproche « émile » et « zola »
/// sur l'ensemble des auteurs d'une notice, et remonte donc l'œuvre de sa fille.
/// </summary>
[Fact]
public void Ecarte_les_ouvrages_ecrits_par_quelqu_un_d_autre()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Zola), "Émile Zola");
Assert.True(
resultat.EcarteesAutreAuteur > 0,
"la fixture Zola doit contenir des notices d'un autre auteur, sinon elle ne teste rien");
// Denise Le Blond-Zola a écrit sur son père ; ses livres ne sont pas de lui.
Assert.DoesNotContain(
resultat.Notices,
n => n.Auteurs.Any(a => a.Contains("Le Blond", StringComparison.OrdinalIgnoreCase)));
Assert.All(
resultat.Notices,
n => Assert.Contains(
n.Auteurs,
a => MaBibli.Shared.Textes.RapprochementAuteurs.SontLeMemeAuteur(a, "Émile Zola")));
}
[Fact]
public void Tolere_la_forme_bibliographique_du_nom()
{
// La notice porte « Werber, Bernard (1961-....). Auteur du texte » : c'est le nettoyage
// ISBD puis le rapprochement d'auteurs qui doivent le ramener à « Bernard Werber ».
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "werber, bernard");
Assert.NotEmpty(resultat.Notices);
}
[Fact]
public void Extrait_l_ISBN_du_champ_identifier()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
var avecIsbn = resultat.Notices.Where(n => !string.IsNullOrWhiteSpace(n.Isbn)).ToList();
Assert.NotEmpty(avecIsbn);
// Le préfixe « ISBN » du dc:identifier ne doit pas rester collé au numéro.
Assert.All(avecIsbn, n => Assert.DoesNotContain("ISBN", n.Isbn!, StringComparison.OrdinalIgnoreCase));
Assert.All(avecIsbn, n => Assert.True(n.Isbn!.Length is 10 or 13, n.Isbn));
}
[Fact]
public void Coupe_la_mention_de_responsabilite_du_titre()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
// « Troisième humanité / Bernard Werber » → « Troisième humanité ».
Assert.All(resultat.Notices, n => Assert.DoesNotContain(" / ", n.Titre));
}
/// <summary>
/// Le regroupement est ce qui rend l'écran utilisable : sans lui, « Les fourmis » occupe
/// autant de lignes qu'il y a eu de retirages.
/// </summary>
[Fact]
public void Regroupe_les_reeditions_sous_une_seule_oeuvre()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
var oeuvres = ServiceBibliographie.Regrouper(resultat).ToList();
Assert.True(
oeuvres.Count < resultat.Notices.Count,
$"{resultat.Notices.Count} notices devraient donner moins de {resultat.Notices.Count} œuvres");
// Aucune œuvre en double après regroupement.
var cles = oeuvres.Select(o => MaBibli.Shared.Textes.CleOeuvre.Cle(o.Titre)).ToList();
Assert.Equal(cles.Count, cles.Distinct().Count());
// Au moins une œuvre a bien réuni plusieurs notices, sinon le test ne prouve rien.
Assert.Contains(oeuvres, o => o.NombreEditions > 1);
}
[Fact]
public void Retient_l_edition_la_plus_ancienne_comme_representant()
{
var resultat = BnfBibliographieParser.Parser(Fixture.Lire(Werber), "Bernard Werber");
foreach (var oeuvre in ServiceBibliographie.Regrouper(resultat).Where(o => o.NombreEditions > 1))
{
var cle = MaBibli.Shared.Textes.CleOeuvre.Cle(oeuvre.Titre);
var anneesDuGroupe = resultat.Notices
.Where(n => n.CleTitre == cle)
.Select(n => ServiceBibliographie.AnneeTriable(n.Annee))
.Where(a => a is not null)
.Select(a => a!.Value)
.ToList();
if (anneesDuGroupe.Count == 0)
{
continue;
}
Assert.Equal(anneesDuGroupe.Min(), ServiceBibliographie.AnneeTriable(oeuvre.Annee));
}
}
[Fact]
public void Une_reponse_vide_ne_fait_pas_echouer_la_lecture()
{
const string vide = """
<srw:searchRetrieveResponse xmlns:srw="http://www.loc.gov/zing/srw/">
<srw:numberOfRecords>0</srw:numberOfRecords>
</srw:searchRetrieveResponse>
""";
var resultat = BnfBibliographieParser.Parser(vide, "Personne");
Assert.Empty(resultat.Notices);
Assert.Equal(0, resultat.NombreAnnonce);
}
[Theory]
[InlineData("2012", 2012)]
[InlineData("[DL 2012]", 2012)]
[InlineData("cop. 1991", 1991)]
[InlineData("19..?", null)]
[InlineData("", null)]
[InlineData(null, null)]
public void Extrait_une_annee_triable_des_dates_BnF(string? date, int? attendu) =>
Assert.Equal(attendu, ServiceBibliographie.AnneeTriable(date));
}