Gestion de bibliothèque personnelle auto-hébergée : catalogue, prêts, scan de code-barres, consultation hors-ligne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
224 lines
8.1 KiB
C#
224 lines
8.1 KiB
C#
using MaBibli.Shared.Catalogue;
|
|
using MaBibli.Shared.Entites;
|
|
|
|
namespace MaBibli.Tests;
|
|
|
|
/// <summary>
|
|
/// Filtrage et recherche du catalogue. Purement en mémoire : ni base, ni réseau.
|
|
/// </summary>
|
|
public class FiltreLivresTests
|
|
{
|
|
private static Auteur Auteur(int id, string nom)
|
|
{
|
|
var auteur = new Auteur { Id = id, Nom = nom };
|
|
auteur.RecalculerFormes();
|
|
return auteur;
|
|
}
|
|
|
|
private static readonly Auteur Zola = Auteur(1, "Émile Zola");
|
|
private static readonly Auteur Maupassant = Auteur(2, "Guy de Maupassant");
|
|
|
|
private static Livre Livre(
|
|
int id, string titre, Auteur? auteur, Format format, Statut? statut, string? ajoutePar,
|
|
string? lecteur = "mathieu")
|
|
{
|
|
var livre = new Livre
|
|
{
|
|
Id = id,
|
|
Titre = titre,
|
|
Format = format,
|
|
AjoutePar = ajoutePar,
|
|
};
|
|
|
|
livre.RecalculerFormes();
|
|
|
|
if (auteur is not null)
|
|
{
|
|
livre.Auteurs.Add(new LivreAuteur { LivreId = id, AuteurId = auteur.Id, Auteur = auteur });
|
|
}
|
|
|
|
if (statut is { } valeur && lecteur is not null)
|
|
{
|
|
livre.Statuts.Add(new StatutLecture { LivreId = id, Utilisateur = lecteur, Statut = valeur });
|
|
}
|
|
|
|
return livre;
|
|
}
|
|
|
|
// « mathieu » a un statut sur les quatre livres ; « camille » n'en a aucun.
|
|
private static readonly Livre[] Catalogue =
|
|
[
|
|
Livre(1, "Germinal", Zola, Format.Physique, Statut.Lu, "mathieu"),
|
|
Livre(2, "La Bête humaine", Zola, Format.Numerique, Statut.ALire, "camille"),
|
|
Livre(3, "Le Horla", Maupassant, Format.Physique, Statut.EnCours, "camille"),
|
|
Livre(4, "Bel-Ami", null, Format.Numerique, Statut.Lu, null),
|
|
];
|
|
|
|
private static List<Livre> Filtrer(CritereLivres criteres, string? utilisateur = "mathieu") =>
|
|
FiltreLivres.Appliquer(Catalogue.AsQueryable(), criteres, utilisateur).ToList();
|
|
|
|
[Fact]
|
|
public void Sans_critere_tout_le_catalogue_remonte()
|
|
=> Assert.Equal(4, Filtrer(new CritereLivres()).Count);
|
|
|
|
[Fact]
|
|
public void Le_tri_est_alphabetique_sur_le_titre()
|
|
=> Assert.Equal(
|
|
["Bel-Ami", "Germinal", "La Bête humaine", "Le Horla"],
|
|
Filtrer(new CritereLivres()).Select(l => l.Titre));
|
|
|
|
[Theory]
|
|
[InlineData(Format.Physique, 2)]
|
|
[InlineData(Format.Numerique, 2)]
|
|
public void Le_filtre_format_ne_garde_que_ce_format(Format format, int attendu)
|
|
{
|
|
var resultat = Filtrer(new CritereLivres { Format = format });
|
|
|
|
Assert.Equal(attendu, resultat.Count);
|
|
Assert.All(resultat, l => Assert.Equal(format, l.Format));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(Statut.ALire, 1)]
|
|
[InlineData(Statut.EnCours, 1)]
|
|
[InlineData(Statut.Lu, 2)]
|
|
public void Le_filtre_statut_ne_garde_que_ce_statut(Statut statut, int attendu)
|
|
=> Assert.Equal(attendu, Filtrer(new CritereLivres { Statut = statut }).Count);
|
|
|
|
/// <summary>
|
|
/// <b>Le statut est personnel.</b> Le même filtre, la même bibliothèque, une autre personne :
|
|
/// rien ne remonte, parce que « camille » n'a posé aucun statut.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Le_filtre_statut_ne_voit_que_le_statut_de_lappelant()
|
|
{
|
|
Assert.Equal(2, Filtrer(new CritereLivres { Statut = Statut.Lu }, "mathieu").Count);
|
|
Assert.Empty(Filtrer(new CritereLivres { Statut = Statut.Lu }, "camille"));
|
|
|
|
// Sans filtre, en revanche, camille voit toute la bibliothèque : elle est commune.
|
|
Assert.Equal(4, Filtrer(new CritereLivres(), "camille").Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sans_identite_aucun_statut_ne_correspond()
|
|
=> Assert.Empty(Filtrer(new CritereLivres { Statut = Statut.Lu }, null));
|
|
|
|
[Fact]
|
|
public void La_recherche_porte_sur_le_titre()
|
|
=> Assert.Equal(["Germinal"], Filtrer(new CritereLivres { Recherche = "germ" }).Select(l => l.Titre));
|
|
|
|
[Fact]
|
|
public void La_recherche_porte_aussi_sur_lauteur()
|
|
=> Assert.Equal(
|
|
["Germinal", "La Bête humaine"],
|
|
Filtrer(new CritereLivres { Recherche = "zola" }).Select(l => l.Titre));
|
|
|
|
/// <summary>
|
|
/// Le besoin exprimé dans CLAUDE.md : « Emile » doit trouver « Émile ». Il n'y a pas de
|
|
/// collation SQLite pour ça, seule la colonne normalisée le permet.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("emile")]
|
|
[InlineData("Émile")]
|
|
[InlineData("EMILE ZOLA")]
|
|
[InlineData("zola, émile")]
|
|
public void La_recherche_ignore_les_accents_sur_lauteur(string terme)
|
|
=> Assert.Equal(
|
|
["Germinal", "La Bête humaine"],
|
|
Filtrer(new CritereLivres { Recherche = terme }).Select(l => l.Titre));
|
|
|
|
[Theory]
|
|
[InlineData("bete")]
|
|
[InlineData("BÊTE")]
|
|
public void La_recherche_ignore_les_accents_sur_le_titre(string terme)
|
|
=> Assert.Equal(["La Bête humaine"], Filtrer(new CritereLivres { Recherche = terme }).Select(l => l.Titre));
|
|
|
|
[Theory]
|
|
[InlineData("HORLA")]
|
|
[InlineData("horla")]
|
|
[InlineData(" Horla ")]
|
|
public void La_recherche_ignore_la_casse_et_les_espaces_autour(string terme)
|
|
=> Assert.Single(Filtrer(new CritereLivres { Recherche = terme }));
|
|
|
|
[Fact]
|
|
public void Un_livre_sans_auteur_ne_fait_pas_echouer_la_recherche()
|
|
{
|
|
// Le livre 4 n'a aucun auteur : la clause sur les auteurs doit le traverser sans exploser.
|
|
var resultat = Filtrer(new CritereLivres { Recherche = "bel" });
|
|
|
|
Assert.Equal(["Bel-Ami"], resultat.Select(l => l.Titre));
|
|
}
|
|
|
|
[Fact]
|
|
public void Le_filtre_par_auteur_regroupe_ses_livres()
|
|
=> Assert.Equal(
|
|
["Germinal", "La Bête humaine"],
|
|
Filtrer(new CritereLivres { AuteurId = Zola.Id }).Select(l => l.Titre));
|
|
|
|
[Fact]
|
|
public void Les_criteres_se_combinent()
|
|
{
|
|
var resultat = Filtrer(new CritereLivres
|
|
{
|
|
Recherche = "zola",
|
|
Format = Format.Physique,
|
|
Statut = Statut.Lu,
|
|
});
|
|
|
|
Assert.Equal(["Germinal"], resultat.Select(l => l.Titre));
|
|
}
|
|
|
|
[Fact]
|
|
public void Une_recherche_vide_equivaut_a_aucune_recherche()
|
|
{
|
|
Assert.Equal(4, Filtrer(new CritereLivres { Recherche = " " }).Count);
|
|
Assert.Equal(4, Filtrer(new CritereLivres { Recherche = "" }).Count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <b>Invariant le plus important du projet.</b> La bibliothèque est commune au foyer :
|
|
/// une lecture ne doit JAMAIS être restreinte à ce qu'a saisi l'utilisateur courant.
|
|
/// Ce test échouerait au moindre filtre ajouté sur <c>AjoutePar</c>.
|
|
/// </summary>
|
|
[Fact]
|
|
public void La_lecture_ne_filtre_jamais_sur_AjoutePar()
|
|
{
|
|
var resultat = Filtrer(new CritereLivres());
|
|
|
|
// Les livres des trois provenances (deux comptes + un sans trace) sont tous là.
|
|
Assert.Contains(resultat, l => l.AjoutePar == "mathieu");
|
|
Assert.Contains(resultat, l => l.AjoutePar == "camille");
|
|
Assert.Contains(resultat, l => l.AjoutePar is null);
|
|
Assert.Equal(Catalogue.Length, resultat.Count);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Second verrou sur le même invariant, par le type : aucun critère de consultation ne doit
|
|
/// permettre d'exprimer un cloisonnement par utilisateur. Si quelqu'un ajoute une propriété
|
|
/// « AjoutePar » à <see cref="CritereLivres"/>, ce test le lui rappelle.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Les_criteres_nexposent_aucun_champ_dutilisateur()
|
|
{
|
|
var proprietes = typeof(CritereLivres)
|
|
.GetProperties()
|
|
.Select(p => p.Name)
|
|
.ToArray();
|
|
|
|
Assert.DoesNotContain(proprietes, nom => nom.Contains("Ajoute", StringComparison.OrdinalIgnoreCase));
|
|
Assert.DoesNotContain(proprietes, nom => nom.Contains("Utilisateur", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Troisième verrou, sur la décision inverse : <see cref="Livre"/> ne doit plus jamais porter
|
|
/// de colonne <c>Statut</c>, qui redeviendrait commune à tout le foyer.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Livre_ne_porte_aucun_statut_commun()
|
|
{
|
|
var statut = typeof(Livre).GetProperty("Statut");
|
|
|
|
Assert.Null(statut);
|
|
}
|
|
}
|