MaBibli 1.0.0
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>
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user