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>
290 lines
11 KiB
C#
290 lines
11 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 Lordre_des_sous_series_se_reordonne_sans_toucher_aux_tomes()
|
|
{
|
|
var cycle = await SerieAsync("Cycle");
|
|
var une = await SerieAsync("Une", cycle.Id);
|
|
var deux = await SerieAsync("Deux", cycle.Id);
|
|
|
|
var apres = (await _series.ReordonnerSousSeriesAsync(cycle.Id, [deux.Id, une.Id])).Serie!;
|
|
|
|
Assert.Equal([deux.Id, une.Id],
|
|
(await _series.ListerAsync()).Where(s => s.SerieParenteId == cycle.Id)
|
|
.OrderBy(s => s.Position).Select(s => s.Id));
|
|
Assert.Empty(apres.Elements);
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|