Files
mabibli/MaBibli.Tests/ServiceSeriesTests.cs
T
mathieuandClaude Opus 5 4e372ab694 Ajouter les sagas et cycles, avec leurs tomes manquants
Première relation entre livres du projet. Une série contient des séries
(un cycle EST une série de séries) et des PLACES dans l'ordre de lecture —
pas des livres : une place sans livre est le trou qu'on vient voir.

L'ordre est stocké, jamais déduit d'une année : une préquelle se lit avant
le livre paru dix ans plus tôt. Les tomes non possédés se saisissent à la
main, aucune source ne donnant l'ordre de lecture d'une saga.

Le titre de la place survit à la suppression du livre (SetNull, pas de
cascade), sans quoi perdre un exemplaire effacerait le tome 3 de la série.

Les séries sont communes au foyer. Seule exception : la mise en envies d'un
tome manquant, qui écrit dans une liste personnelle — c'est le seul point
où les deux portées se rencontrent.

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

275 lines
10 KiB
C#

using MaBibli.Api.Data;
using MaBibli.Api.Services.Catalogue;
using MaBibli.Api.Services.Prets;
using MaBibli.Api.Services.Series;
using MaBibli.Api.Services.Souhaits;
using MaBibli.Shared.Dtos;
using MaBibli.Shared.Entites;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
namespace MaBibli.Tests;
/// <summary>
/// Sagas, cycles et séries, sur une base SQLite <b>en mémoire</b> : les invariants tenus ici
/// sont pour moitié des index (unicité du titre, un livre une seule fois par série) et ne se
/// vérifieraient pas sur un faux dépôt.
/// </summary>
/// <remarks>
/// Ce qui est protégé en priorité : <b>un tome sans livre est normal</b> (c'est le trou qu'on
/// vient voir), une suppression de livre <b>laisse</b> le trou, et l'ordre de lecture est
/// stocké — jamais déduit.
/// </remarks>
public class ServiceSeriesTests : IDisposable
{
private readonly SqliteConnection _connexion;
private readonly MaBibliDbContext _db;
private readonly ServiceSeries _series;
private readonly ServiceCatalogue _catalogue;
private readonly ServiceSouhaits _souhaits;
private const string Mathieu = "mathieu";
private const string Camille = "camille";
public ServiceSeriesTests()
{
_connexion = new SqliteConnection("Data Source=:memory:");
_connexion.Open();
var options = new DbContextOptionsBuilder<MaBibliDbContext>()
.UseSqlite(_connexion)
.Options;
_db = new MaBibliDbContext(options);
_db.Database.EnsureCreated();
_souhaits = new ServiceSouhaits(_db);
_series = new ServiceSeries(_db, _souhaits);
_catalogue = new ServiceCatalogue(_db, new ServiceAuteurs(_db));
}
private async Task<int> LivreAsync(string titre, string auteur = "R. A. Salvatore")
{
var resultat = await _catalogue.CreerAsync(
new EnregistrementLivre { Titre = titre, Auteur = auteur }, Mathieu, confirmerDoublon: true);
return resultat.Livre!.Id;
}
private async Task<SerieDto> SerieAsync(string titre, int? parenteId = null)
{
var resultat = await _series.CreerAsync(
new EnregistrementSerie { Titre = titre, SerieParenteId = parenteId }, Mathieu);
Assert.Null(resultat.Erreur);
return resultat.Serie!;
}
[Fact]
public async Task Une_serie_melange_tomes_possedes_et_tomes_manquants()
{
var serie = await SerieAsync("L'Elfe noir");
var livreId = await LivreAsync("Terre natale");
await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });
var apres = (await _series.AjouterElementAsync(
serie.Id, new AjoutElementSerie { Titre = "Terre d'exil" })).Serie!;
Assert.Equal(["Terre natale", "Terre d'exil"], apres.Elements.Select(e => e.Titre));
Assert.Equal([true, false], apres.Elements.Select(e => e.Possede));
// Le compteur de l'écran : « 1 sur 2 ».
Assert.Equal(1, apres.NombrePossedes);
}
[Fact]
public async Task Un_tome_sans_titre_ni_livre_est_refuse()
{
var serie = await SerieAsync("L'Elfe noir");
var resultat = await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie());
Assert.NotNull(resultat.Erreur);
}
[Fact]
public async Task Supprimer_un_livre_laisse_un_trou_nomme_dans_la_serie()
{
var serie = await SerieAsync("L'Elfe noir");
var livreId = await LivreAsync("Terre natale");
await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });
await _catalogue.SupprimerAsync(livreId);
// ⚠️ L'invariant décisif : la place survit, avec son titre. Une cascade effacerait le
// tome 1 de la saga parce qu'on a perdu son exemplaire.
var element = (await _series.ListerAsync()).Single().Elements.Single();
Assert.Equal("Terre natale", element.Titre);
Assert.False(element.Possede);
}
[Fact]
public async Task Lordre_de_lecture_est_stocke_et_se_reordonne()
{
var serie = await SerieAsync("L'Elfe noir");
var avant = serie;
foreach (var titre in new[] { "Terre natale", "Terre d'exil", "Terre promise" })
{
avant = (await _series.AjouterElementAsync(
serie.Id, new AjoutElementSerie { Titre = titre })).Serie!;
}
var inverse = avant.Elements.Select(e => e.Id).Reverse().ToList();
var apres = (await _series.ReordonnerAsync(serie.Id, inverse)).Serie!;
Assert.Equal(["Terre promise", "Terre d'exil", "Terre natale"], apres.Elements.Select(e => e.Titre));
Assert.Equal([0, 1, 2], apres.Elements.Select(e => e.Position));
}
[Fact]
public async Task Un_tome_absent_de_lordre_recu_est_conserve_a_la_suite()
{
var serie = await SerieAsync("L'Elfe noir");
var un = (await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { Titre = "Un" })).Serie!;
await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { Titre = "Deux" });
// Le client ne connaissait que le premier tome : le second ne doit pas disparaître.
var apres = (await _series.ReordonnerAsync(serie.Id, [un.Elements[0].Id])).Serie!;
Assert.Equal(["Un", "Deux"], apres.Elements.Select(e => e.Titre));
}
[Fact]
public async Task Un_meme_livre_ne_tient_quune_place_dans_une_serie_mais_vit_dans_plusieurs()
{
var serie = await SerieAsync("L'Elfe noir");
var autre = await SerieAsync("Les Royaumes oubliés");
var livreId = await LivreAsync("Terre natale");
await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });
var doublon = await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });
var ailleurs = await _series.AjouterElementAsync(autre.Id, new AjoutElementSerie { LivreId = livreId });
Assert.NotNull(doublon.Erreur);
// Un livre appartient légitimement à plusieurs regroupements : une série, un cycle,
// une intégrale.
Assert.Null(ailleurs.Erreur);
}
[Fact]
public async Task Une_serie_se_range_dans_un_cycle_mais_jamais_dans_elle_meme()
{
var cycle = await SerieAsync("La Légende de Drizzt");
var trilogie = await SerieAsync("L'Elfe noir");
var range = await _series.ModifierAsync(
trilogie.Id, new EnregistrementSerie { Titre = trilogie.Titre, SerieParenteId = cycle.Id });
Assert.Equal(cycle.Id, range.Serie!.SerieParenteId);
// Refermer l'arbre sur lui-même ferait boucler l'affichage à l'infini.
var boucle = await _series.ModifierAsync(
cycle.Id, new EnregistrementSerie { Titre = cycle.Titre, SerieParenteId = trilogie.Id });
Assert.NotNull(boucle.Erreur);
}
[Fact]
public async Task Supprimer_un_cycle_ne_detruit_pas_les_series_quil_contient()
{
var cycle = await SerieAsync("La Légende de Drizzt");
var trilogie = await SerieAsync("L'Elfe noir", cycle.Id);
await _series.SupprimerAsync(cycle.Id);
var restantes = await _series.ListerAsync();
var seule = Assert.Single(restantes);
Assert.Equal(trilogie.Id, seule.Id);
Assert.Null(seule.SerieParenteId);
}
[Fact]
public async Task Deux_series_ne_peuvent_pas_porter_le_meme_titre()
{
await SerieAsync("L'Elfe noir");
var doublon = await _series.CreerAsync(
new EnregistrementSerie { Titre = "l'elfe noir" }, Mathieu);
Assert.NotNull(doublon.Erreur);
}
[Fact]
public async Task Un_tome_manquant_part_dans_la_liste_denvies_de_lappelant()
{
var serie = await SerieAsync("L'Elfe noir");
var apres = (await _series.AjouterElementAsync(
serie.Id, new AjoutElementSerie { Titre = "Terre d'exil" })).Serie!;
var resultat = await _series.MettreEnEnviesAsync(apres.Elements[0].Id, Camille);
Assert.Null(resultat.Erreur);
// ⚠️ La série est COMMUNE, l'envie est PERSONNELLE : elle n'apparaît que chez Camille.
Assert.Equal(["Terre d'exil"], (await _souhaits.ListerAsync(Camille)).Select(s => s.Titre));
Assert.Empty(await _souhaits.ListerAsync(Mathieu));
}
[Fact]
public async Task Un_tome_deja_possede_ne_part_pas_en_envies()
{
var serie = await SerieAsync("L'Elfe noir");
var livreId = await LivreAsync("Terre natale");
var apres = (await _series.AjouterElementAsync(
serie.Id, new AjoutElementSerie { LivreId = livreId })).Serie!;
var resultat = await _series.MettreEnEnviesAsync(apres.Elements[0].Id, Mathieu);
Assert.NotNull(resultat.Erreur);
Assert.Empty(await _souhaits.ListerAsync(Mathieu));
}
[Fact]
public async Task Detacher_un_livre_laisse_la_place_avec_son_titre()
{
var serie = await SerieAsync("L'Elfe noir");
var livreId = await LivreAsync("Terre natale");
var apres = (await _series.AjouterElementAsync(
serie.Id, new AjoutElementSerie { LivreId = livreId, Titre = "Tome 1" })).Serie!;
var detache = (await _series.RattacherLivreAsync(apres.Elements[0].Id, null)).Serie!;
Assert.False(detache.Elements[0].Possede);
Assert.Equal("Tome 1", detache.Elements[0].Titre);
// Le livre, lui, n'a pas bougé du catalogue.
Assert.NotNull(await _catalogue.ObtenirAsync(livreId, Mathieu));
}
[Fact]
public async Task Un_tome_prete_le_dit_dans_la_serie()
{
var serie = await SerieAsync("L'Elfe noir");
var livreId = await LivreAsync("Terre natale");
await _series.AjouterElementAsync(serie.Id, new AjoutElementSerie { LivreId = livreId });
var prets = new ServicePrets(_db);
await prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" });
var element = (await _series.ListerAsync()).Single().Elements.Single();
// « Où est le tome 1 ? » a une seule réponse, la même pour tout le foyer.
Assert.Equal("Paul", element.PreteA);
}
public void Dispose()
{
_db.Dispose();
_connexion.Dispose();
GC.SuppressFinalize(this);
}
}