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; /// /// Sagas, cycles et séries, sur une base SQLite en mémoire : 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. /// /// /// Ce qui est protégé en priorité : un tome sans livre est normal (c'est le trou qu'on /// vient voir), une suppression de livre laisse le trou, et l'ordre de lecture est /// stocké — jamais déduit. /// 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() .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 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 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); } }