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

186 lines
6.1 KiB
C#

using System.Text;
using MaBibli.Api.Services.Souhaits;
using MaBibli.Shared.Dtos;
namespace MaBibli.Tests;
/// <summary>
/// Contenu <b>exact</b> des fichiers d'export. Fonctions pures : rien à simuler.
/// </summary>
public class ExportSouhaitsTests
{
private static readonly DateTime Jour = new(2026, 8, 18, 10, 0, 0, DateTimeKind.Utc);
private static SouhaitDto Souhait(
int id, string titre, string? auteur = null, string? editeur = null,
string? annee = null, string? isbn = null, string? note = null) =>
new()
{
Id = id,
Titre = titre,
Auteur = auteur,
Editeur = editeur,
Annee = annee,
Isbn = isbn,
Note = note,
DateAjout = Jour,
};
[Fact]
public void Le_texte_groupe_les_livres_par_auteur()
{
var texte = ExportSouhaits.Texte(
[
Souhait(1, "La prophétie des abeilles", "Bernard Werber"),
Souhait(2, "Germinal", "Émile Zola"),
Souhait(3, "Les fourmis", "Bernard Werber"),
],
"Mathieu",
Jour);
Assert.Contains("Liste d'envies de Mathieu — 18/08/2026", texte);
Assert.Contains("3 livres", texte);
// Un seul en-tête par auteur, et ses deux titres regroupés dessous.
var lignes = texte.Split('\n');
Assert.Single(lignes, l => l == "Bernard Werber");
var debutWerber = Array.IndexOf(lignes, "Bernard Werber");
Assert.Equal(" - La prophétie des abeilles", lignes[debutWerber + 1]);
Assert.Equal(" - Les fourmis", lignes[debutWerber + 2]);
// Werber avant Zola : les auteurs sont classés alphabétiquement.
Assert.True(debutWerber < Array.IndexOf(lignes, "Émile Zola"));
}
[Fact]
public void Le_texte_range_les_livres_sans_auteur_en_dernier()
{
var texte = ExportSouhaits.Texte(
[Souhait(1, "Titre orphelin"), Souhait(2, "Germinal", "Émile Zola")],
null,
Jour);
Assert.True(texte.IndexOf("Émile Zola", StringComparison.Ordinal)
< texte.IndexOf("Auteur non précisé", StringComparison.Ordinal));
}
[Fact]
public void Le_texte_porte_les_precisions_utiles_en_librairie()
{
var texte = ExportSouhaits.Texte(
[Souhait(1, "Germinal", "Émile Zola", "Le Livre de poche", "1885", "2253004227", "en poche seulement")],
null,
Jour);
Assert.Contains(" - Germinal (Le Livre de poche, 1885, ISBN 2253004227)", texte);
Assert.Contains(" en poche seulement", texte);
}
[Fact]
public void Le_texte_le_dit_quand_la_liste_est_vide()
{
var texte = ExportSouhaits.Texte([], "Mathieu", Jour);
Assert.Contains("(aucun livre dans la liste)", texte);
}
[Fact]
public void Le_csv_porte_un_en_tete_et_une_ligne_par_envie()
{
var csv = ExportSouhaits.Csv([Souhait(1, "Germinal", "Émile Zola", "Le Livre de poche", "1885")]);
var lignes = csv.Split("\r\n", StringSplitOptions.RemoveEmptyEntries);
Assert.Equal("Titre;Auteur;Éditeur;Année;ISBN;Note;Ajouté le", lignes[0]);
Assert.Equal("Germinal;Émile Zola;Le Livre de poche;1885;;;2026-08-18", lignes[1]);
}
/// <summary>
/// Sans échappement, une note contenant le séparateur décalerait toutes les colonnes
/// suivantes — et une note est du texte parfaitement libre.
/// </summary>
[Fact]
public void Le_csv_echappe_le_separateur_et_les_guillemets()
{
var csv = ExportSouhaits.Csv(
[Souhait(1, "Titre; piégé", note: "il a dit \"oui\"")]);
var ligne = csv.Split("\r\n")[1];
Assert.StartsWith("\"Titre; piégé\";", ligne);
Assert.Contains("\"il a dit \"\"oui\"\"\"", ligne);
// Sept colonnes une fois l'échappement respecté, pas huit.
Assert.Equal(7, ColonnesCsv(ligne).Count);
}
[Fact]
public void Le_csv_echappe_les_sauts_de_ligne()
{
var csv = ExportSouhaits.Csv([Souhait(1, "Titre", note: "ligne 1\nligne 2")]);
Assert.Contains("\"ligne 1\nligne 2\"", csv);
}
/// <summary>
/// Sans BOM, Excel affiche « Émile Zola » — sur un fonds français, à peu près chaque ligne.
/// </summary>
[Fact]
public void Le_bom_precede_le_csv()
{
var octets = ExportSouhaits.Bom
.Concat(Encoding.UTF8.GetBytes(ExportSouhaits.Csv([Souhait(1, "Émile")])))
.ToArray();
Assert.Equal([0xEF, 0xBB, 0xBF], octets[..3]);
}
[Theory]
[InlineData(FormatExportSouhaits.Texte, "liste-envies-2026-08-18.txt", "text/plain; charset=utf-8")]
[InlineData(FormatExportSouhaits.Csv, "liste-envies-2026-08-18.csv", "text/csv; charset=utf-8")]
public void Le_fichier_est_date_et_typé(FormatExportSouhaits format, string nom, string mime)
{
Assert.Equal(nom, ExportSouhaits.NomFichier(format, Jour));
Assert.Equal(mime, ExportSouhaits.TypeMime(format));
}
/// <summary>Découpe une ligne CSV en respectant les guillemets (lecteur minimal, pour le test).</summary>
private static List<string> ColonnesCsv(string ligne)
{
var colonnes = new List<string>();
var courante = new StringBuilder();
var dansGuillemets = false;
for (var i = 0; i < ligne.Length; i++)
{
var c = ligne[i];
if (c == '"')
{
if (dansGuillemets && i + 1 < ligne.Length && ligne[i + 1] == '"')
{
courante.Append('"');
i++;
continue;
}
dansGuillemets = !dansGuillemets;
continue;
}
if (c == ExportSouhaits.SeparateurCsv && !dansGuillemets)
{
colonnes.Add(courante.ToString());
courante.Clear();
continue;
}
courante.Append(c);
}
colonnes.Add(courante.ToString());
return colonnes;
}
}