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

220 lines
7.5 KiB
C#

using MaBibli.Api.Data;
using MaBibli.Api.Services.Catalogue;
using MaBibli.Shared.Catalogue;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Entites;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace MaBibli.Tests;
/// <summary>
/// CRUD du catalogue, sur une base SQLite <b>en mémoire</b> : même moteur qu'en production,
/// aucun fichier, aucun réseau.
/// </summary>
public class ServiceCatalogueTests : IDisposable
{
private readonly SqliteConnection _connexion;
private readonly MaBibliDbContext _db;
private readonly ServiceCatalogue _service;
public ServiceCatalogueTests()
{
// La connexion doit rester ouverte : SQLite détruit une base « :memory: » à sa fermeture.
_connexion = new SqliteConnection("Data Source=:memory:");
_connexion.Open();
var options = new DbContextOptionsBuilder<MaBibliDbContext>()
.UseSqlite(_connexion)
.Options;
_db = new MaBibliDbContext(options);
_db.Database.EnsureCreated();
_service = new ServiceCatalogue(_db);
}
private static EnregistrementLivre Saisie(
string titre,
string? auteur = null,
string? isbn = null,
Format format = Format.Physique,
Statut statut = Statut.ALire) => new()
{
Titre = titre,
Auteur = auteur,
Isbn = isbn,
Format = format,
Statut = statut,
};
[Fact]
public async Task Creer_enregistre_le_livre_et_trace_lauteur_de_la_saisie()
{
var resultat = await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
Assert.Null(resultat.Erreur);
var livre = resultat.Livre!;
Assert.True(livre.Id > 0);
Assert.Equal("Germinal", livre.Titre);
Assert.Equal("mathieu", livre.AjoutePar);
Assert.NotEqual(default, livre.DateAjout);
}
[Fact]
public async Task Creer_normalise_lisbn()
{
var resultat = await _service.CreerAsync(Saisie("Germinal", isbn: "978-2-253-00422-6"), "mathieu");
Assert.Equal("9782253004226", resultat.Livre!.Isbn);
}
[Fact]
public async Task Creer_refuse_un_titre_vide()
{
var resultat = await _service.CreerAsync(Saisie(" "), "mathieu");
Assert.Null(resultat.Livre);
Assert.NotNull(resultat.Erreur);
}
[Fact]
public async Task Creer_refuse_un_isbn_invalide_mais_accepte_labsence_disbn()
{
var invalide = await _service.CreerAsync(Saisie("Sans titre", isbn: "1234567890123"), "mathieu");
Assert.Null(invalide.Livre);
Assert.NotNull(invalide.Erreur);
// Certains livres n'ont pas d'ISBN : l'absence doit rester parfaitement légitime.
var sansIsbn = await _service.CreerAsync(Saisie("Carnet de famille"), "mathieu");
Assert.Null(sansIsbn.Erreur);
Assert.Null(sansIsbn.Livre!.Isbn);
}
[Fact]
public async Task Le_catalogue_est_unique_physiques_et_numeriques_cohabitent()
{
await _service.CreerAsync(Saisie("Papier", format: Format.Physique), "mathieu");
await _service.CreerAsync(Saisie("Epub", format: Format.Numerique), "mathieu");
var tout = await _service.ListerAsync(new CritereLivres());
Assert.Equal(2, tout.Count);
var numeriques = await _service.ListerAsync(new CritereLivres { Format = Format.Numerique });
Assert.Equal(["Epub"], numeriques.Select(l => l.Titre));
}
[Fact]
public async Task La_recherche_traverse_bien_SQLite()
{
await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
await _service.CreerAsync(Saisie("Le Horla", "Guy de Maupassant"), "mathieu");
var parAuteur = await _service.ListerAsync(new CritereLivres { Recherche = "maupassant" });
Assert.Equal(["Le Horla"], parAuteur.Select(l => l.Titre));
var parTitre = await _service.ListerAsync(new CritereLivres { Recherche = "GERMI" });
Assert.Equal(["Germinal"], parTitre.Select(l => l.Titre));
}
/// <summary>
/// <b>Invariant central</b> : la bibliothèque est commune. Des livres saisis par des comptes
/// différents doivent tous ressortir, quelle que soit la personne qui consulte.
/// </summary>
[Fact]
public async Task La_liste_renvoie_les_livres_de_tous_les_utilisateurs()
{
await _service.CreerAsync(Saisie("Livre de Mathieu"), "mathieu");
await _service.CreerAsync(Saisie("Livre de Camille"), "camille");
await _service.CreerAsync(Saisie("Livre anonyme"), null);
var tout = await _service.ListerAsync(new CritereLivres());
Assert.Equal(3, tout.Count);
Assert.Contains(tout, l => l.AjoutePar == "mathieu");
Assert.Contains(tout, l => l.AjoutePar == "camille");
Assert.Contains(tout, l => l.AjoutePar is null);
}
[Fact]
public async Task Modifier_ne_reecrit_ni_AjoutePar_ni_DateAjout()
{
var cree = (await _service.CreerAsync(Saisie("Titre initial", "Auteur"), "camille")).Livre!;
var modifie = await _service.ModifierAsync(cree.Id, new EnregistrementLivre
{
Titre = "Titre corrigé",
Auteur = "Auteur corrigé",
Format = Format.Numerique,
Statut = Statut.Lu,
});
Assert.Null(modifie.Erreur);
Assert.Equal("Titre corrigé", modifie.Livre!.Titre);
Assert.Equal(Format.Numerique, modifie.Livre.Format);
// La trace de la saisie d'origine survit à l'édition, y compris par quelqu'un d'autre.
Assert.Equal("camille", modifie.Livre.AjoutePar);
Assert.Equal(cree.DateAjout, modifie.Livre.DateAjout);
}
[Fact]
public async Task Modifier_un_livre_inexistant_est_signale_comme_introuvable()
{
var resultat = await _service.ModifierAsync(4242, Saisie("Fantôme"));
Assert.True(resultat.EstIntrouvable);
}
[Fact]
public async Task Changer_le_statut_ne_touche_que_le_statut()
{
var cree = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
var apres = await _service.ChangerStatutAsync(cree.Id, Statut.EnCours);
Assert.NotNull(apres);
Assert.Equal(Statut.EnCours, apres!.Statut);
Assert.Equal(cree.Titre, apres.Titre);
Assert.Equal(cree.Auteur, apres.Auteur);
Assert.Equal(cree.AjoutePar, apres.AjoutePar);
}
[Fact]
public async Task Changer_le_statut_dun_livre_inexistant_renvoie_null()
=> Assert.Null(await _service.ChangerStatutAsync(4242, Statut.Lu));
[Fact]
public async Task Supprimer_retire_le_livre_du_catalogue()
{
var cree = (await _service.CreerAsync(Saisie("À jeter"), "mathieu")).Livre!;
Assert.True(await _service.SupprimerAsync(cree.Id));
Assert.Null(await _service.ObtenirAsync(cree.Id));
Assert.Empty(await _service.ListerAsync(new CritereLivres()));
// Seconde suppression : plus rien à supprimer.
Assert.False(await _service.SupprimerAsync(cree.Id));
}
[Fact]
public async Task Obtenir_renvoie_la_fiche_complete()
{
var cree = (await _service.CreerAsync(
Saisie("Le Horla", "Guy de Maupassant", "9782080704092"), "mathieu")).Livre!;
var relu = await _service.ObtenirAsync(cree.Id);
Assert.NotNull(relu);
Assert.Equal("Le Horla", relu!.Titre);
Assert.Equal("9782080704092", relu.Isbn);
}
public void Dispose()
{
_db.Dispose();
_connexion.Dispose();
GC.SuppressFinalize(this);
}
}