Files
mabibli/MaBibli.Tests/FiltreLivresTests.cs
T
mathieuandClaude Opus 5 fd6438b912 Phase 3 : CRUD des livres, statuts de lecture et identite SSOwat
API
- CRUD complet sur /api/livres : liste filtrable (format, statut) et
  cherchable (titre, auteur), consultation, creation, modification,
  changement de statut isole, suppression.
- Aucune lecture ne filtre sur AjoutePar : la bibliotheque est commune au
  foyer. Le critere n'existe meme pas dans CritereLivres, pour qu'on ne
  puisse pas s'en servir par inadvertance. Deux tests verrouillent
  l'invariant, dont un par reflexion sur le type des criteres.
- AjoutePar est renseigne par le serveur a la creation, jamais par le
  client, et n'est pas reecrit a l'edition (pas plus que DateAjout).
- Identite lue dans les en-tetes SSOwat (YNH_USER, YNH_USER_EMAIL,
  YNH_USER_FULLNAME), avec repli sur un utilisateur simule configure en
  developpement uniquement. Sans en-tete ni simulation, l'identite reste
  inconnue plutot qu'inventee. Expose par GET /api/moi.
- Migration appliquee au demarrage : pas d'etape manuelle a l'installation.

Interface Blazor, pensee mobile d'abord
- Catalogue unique (physiques et numeriques melanges, distingues par le
  format) avec recherche, filtres et changement de statut directement dans
  la liste.
- Ajout par ISBN : quand la cascade rend plusieurs notices, un ecran de
  choix les presente en mettant en avant editeur et annee, seuls elements
  qui les departagent. Le formulaire reste ensuite entierement modifiable.
- Ajout manuel, edition, suppression avec confirmation explicite.
- Couvertures : l'URL OpenLibrary n'est jamais validee par un appel
  prealable (502 intermittents mesures en phase 2) ; un attribut onerror
  masque l'image cassee et laisse apparaitre un substitut.

Decisions prises la ou CLAUDE.md etait muet
- Tri alphabetique par titre.
- Un ISBN saisi doit etre valide, mais reste facultatif.
- Ecran de choix affiche seulement a partir de deux candidats.

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

151 lines
5.2 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 readonly Livre[] Catalogue =
[
new()
{
Id = 1, Titre = "Germinal", Auteur = "Émile Zola",
Format = Format.Physique, Statut = Statut.Lu, AjoutePar = "mathieu",
},
new()
{
Id = 2, Titre = "La Bête humaine", Auteur = "Émile Zola",
Format = Format.Numerique, Statut = Statut.ALire, AjoutePar = "camille",
},
new()
{
Id = 3, Titre = "Le Horla", Auteur = "Guy de Maupassant",
Format = Format.Physique, Statut = Statut.EnCours, AjoutePar = "camille",
},
new()
{
Id = 4, Titre = "Bel-Ami", Auteur = null,
Format = Format.Numerique, Statut = Statut.Lu, AjoutePar = null,
},
];
private static List<Livre> Filtrer(CritereLivres criteres) =>
FiltreLivres.Appliquer(Catalogue.AsQueryable(), criteres).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)
{
var resultat = Filtrer(new CritereLivres { Statut = statut });
Assert.Equal(attendu, resultat.Count);
Assert.All(resultat, l => Assert.Equal(statut, l.Statut));
}
[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));
[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 a Auteur = null : la clause sur l'auteur doit le traverser sans exploser.
var resultat = Filtrer(new CritereLivres { Recherche = "bel" });
Assert.Equal(["Bel-Ami"], resultat.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));
}
}