using MaBibli.Api.Data; using MaBibli.Api.Services.Catalogue; using MaBibli.Api.Services.Prets; using MaBibli.Shared.Catalogue; using MaBibli.Shared.Dtos; using MaBibli.Shared.Entites; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace MaBibli.Tests; /// /// Prêts, sur une base SQLite en mémoire : même moteur qu'en production — ce qui compte /// ici, l'index unique partiel « un seul prêt ouvert par livre » n'existant que côté base. /// public class ServicePretsTests : IDisposable { private readonly SqliteConnection _connexion; private readonly MaBibliDbContext _db; private readonly ServicePrets _prets; private readonly ServiceCatalogue _catalogue; public ServicePretsTests() { _connexion = new SqliteConnection("Data Source=:memory:"); _connexion.Open(); var options = new DbContextOptionsBuilder() .UseSqlite(_connexion) .Options; _db = new MaBibliDbContext(options); _db.Database.EnsureCreated(); _prets = new ServicePrets(_db); _catalogue = new ServiceCatalogue(_db, new ServiceAuteurs(_db)); } private async Task LivreAsync(string titre = "Germinal", Format format = Format.Physique) { var resultat = await _catalogue.CreerAsync( new EnregistrementLivre { Titre = titre, Auteur = "Émile Zola", Format = format }, "mathieu"); return resultat.Livre!.Id; } [Fact] public async Task Preter_ouvre_un_pret_sans_date_de_retour() { var livreId = await LivreAsync(); var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" }); Assert.Null(resultat.Erreur); var pret = resultat.Pret!; Assert.Equal("Paul", pret.Emprunteur); Assert.Null(pret.DateRetour); Assert.True(pret.EstEnCours); // Le livre est recopié à plat pour que la vue « prêts en cours » n'ait qu'un appel à faire. Assert.Equal("Germinal", pret.LivreTitre); Assert.Equal("Émile Zola", pret.LivreAuteurs); } [Fact] public async Task Preter_ecarte_les_espaces_autour_de_lemprunteur() { var livreId = await LivreAsync(); var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = " Paul " }); Assert.Equal("Paul", resultat.Pret!.Emprunteur); } [Fact] public async Task Preter_refuse_un_emprunteur_vide() { var livreId = await LivreAsync(); var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = " " }); Assert.NotNull(resultat.Erreur); Assert.Empty(await _db.Prets.ToListAsync()); } [Fact] public async Task Preter_refuse_un_livre_inconnu() { var resultat = await _prets.PreterAsync(999, new EnregistrementPret { Emprunteur = "Paul" }); Assert.True(resultat.EstIntrouvable); } /// /// CLAUDE.md : un ebook n'est qu'une fiche, il n'y a pas d'exemplaire à confier. La règle est /// tenue par l'API, pas seulement par l'interface. /// [Fact] public async Task Preter_refuse_un_livre_numerique() { var livreId = await LivreAsync("Effective Java", Format.Numerique); var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" }); Assert.NotNull(resultat.Erreur); Assert.Contains("numérique", resultat.Erreur); Assert.Empty(await _db.Prets.ToListAsync()); } [Fact] public async Task Preter_refuse_un_livre_deja_sorti_et_nomme_qui_la() { var livreId = await LivreAsync(); await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" }); var resultat = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Marie" }); Assert.NotNull(resultat.Erreur); Assert.Contains("Paul", resultat.Erreur); Assert.Single(await _db.Prets.ToListAsync()); } /// /// Le service vérifie déjà le doublon, mais entre sa vérification et son insertion il y a une /// fenêtre. Ce test contourne volontairement le service pour prouver que la base refuse aussi. /// [Fact] public async Task La_base_refuse_deux_prets_ouverts_sur_le_meme_livre() { var livreId = await LivreAsync(); await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" }); _db.Prets.Add(new Pret { LivreId = livreId, Emprunteur = "Marie", DatePret = DateTime.UtcNow, DateRetour = null, }); await Assert.ThrowsAsync(() => _db.SaveChangesAsync()); } [Fact] public async Task Preter_accepte_une_date_passee() { var livreId = await LivreAsync(); var noel = new DateTime(2025, 12, 25, 0, 0, 0, DateTimeKind.Utc); var resultat = await _prets.PreterAsync( livreId, new EnregistrementPret { Emprunteur = "Paul", DatePret = noel }); Assert.Null(resultat.Erreur); Assert.Equal(noel, resultat.Pret!.DatePret); } [Fact] public async Task Preter_refuse_une_date_future() { var livreId = await LivreAsync(); var resultat = await _prets.PreterAsync( livreId, new EnregistrementPret { Emprunteur = "Paul", DatePret = DateTime.UtcNow.AddDays(30) }); Assert.NotNull(resultat.Erreur); } [Fact] public async Task Rendre_pose_la_date_sans_supprimer_le_pret() { var livreId = await LivreAsync(); var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!; var resultat = await _prets.RendreAsync(pret.Id, new RetourPret()); Assert.Null(resultat.Erreur); Assert.NotNull(resultat.Pret!.DateRetour); Assert.False(resultat.Pret.EstEnCours); // La ligne est toujours là : c'est toute la raison d'être d'une table séparée. Assert.Single(await _db.Prets.ToListAsync()); } [Fact] public async Task Rendre_refuse_un_pret_deja_clos() { var livreId = await LivreAsync(); var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!; await _prets.RendreAsync(pret.Id, new RetourPret()); var resultat = await _prets.RendreAsync(pret.Id, new RetourPret()); Assert.NotNull(resultat.Erreur); } [Fact] public async Task Rendre_refuse_un_pret_inconnu() { Assert.True((await _prets.RendreAsync(999, new RetourPret())).EstIntrouvable); } [Fact] public async Task Rendre_refuse_une_date_anterieure_au_pret() { var livreId = await LivreAsync(); var pret = (await _prets.PreterAsync( livreId, new EnregistrementPret { Emprunteur = "Paul", DatePret = DateTime.UtcNow.AddDays(-3) })).Pret!; var resultat = await _prets.RendreAsync( pret.Id, new RetourPret { DateRetour = DateTime.UtcNow.AddDays(-10) }); Assert.NotNull(resultat.Erreur); } [Fact] public async Task Un_livre_rendu_peut_etre_prete_a_quelquun_dautre() { var livreId = await LivreAsync(); var premier = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!; await _prets.RendreAsync(premier.Id, new RetourPret()); var second = await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Marie" }); Assert.Null(second.Erreur); var historique = await _prets.HistoriqueAsync(livreId); Assert.Equal(2, historique!.Count); // Le prêt clos n'a pas bougé : l'historique conserve bien qui avait le livre avant. Assert.Contains(historique, p => p.Emprunteur == "Paul" && !p.EstEnCours); Assert.Contains(historique, p => p.Emprunteur == "Marie" && p.EstEnCours); } [Fact] public async Task Historique_va_du_plus_recent_au_plus_ancien() { var livreId = await LivreAsync(); foreach (var (nom, jours) in new[] { ("Paul", -90), ("Marie", -30) }) { var pret = (await _prets.PreterAsync( livreId, new EnregistrementPret { Emprunteur = nom, DatePret = DateTime.UtcNow.AddDays(jours) })).Pret!; await _prets.RendreAsync(pret.Id, new RetourPret { DateRetour = DateTime.UtcNow.AddDays(jours + 10) }); } var historique = await _prets.HistoriqueAsync(livreId); Assert.Equal(["Marie", "Paul"], historique!.Select(p => p.Emprunteur)); } [Fact] public async Task Historique_distingue_livre_inconnu_et_livre_jamais_prete() { var livreId = await LivreAsync(); Assert.Null(await _prets.HistoriqueAsync(999)); Assert.Empty((await _prets.HistoriqueAsync(livreId))!); } [Fact] public async Task EnCours_ne_liste_que_les_livres_dehors_du_plus_ancien_au_plus_recent() { var germinal = await LivreAsync("Germinal"); var horla = await LivreAsync("Le Horla"); var petitPrince = await LivreAsync("Le Petit Prince"); // Rendu : ne doit plus apparaître. var rendu = (await _prets.PreterAsync(germinal, new EnregistrementPret { Emprunteur = "Paul" })).Pret!; await _prets.RendreAsync(rendu.Id, new RetourPret()); await _prets.PreterAsync( horla, new EnregistrementPret { Emprunteur = "Marie", DatePret = DateTime.UtcNow.AddDays(-2) }); await _prets.PreterAsync( petitPrince, new EnregistrementPret { Emprunteur = "Jean", DatePret = DateTime.UtcNow.AddDays(-200) }); var enCours = await _prets.EnCoursAsync(); // Le plus ancien d'abord : c'est le livre oublié qu'on cherche dans cette vue. Assert.Equal(["Jean", "Marie"], enCours.Select(p => p.Emprunteur)); Assert.DoesNotContain(enCours, p => p.LivreId == germinal); } /// /// Le catalogue doit signaler qu'un livre est dehors — et cette information est commune /// au foyer, contrairement au statut de lecture. /// [Fact] public async Task Le_catalogue_signale_un_livre_prete_a_tout_le_monde() { var livreId = await LivreAsync(); await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" }); foreach (var utilisateur in new[] { "mathieu", "quelquun-dautre" }) { var livre = await _catalogue.ObtenirAsync(livreId, utilisateur); Assert.Equal("Paul", livre!.PreteA); Assert.True(livre.EstPrete); Assert.NotNull(livre.PreteDepuis); var liste = await _catalogue.ListerAsync(new CritereLivres(), utilisateur); Assert.Equal("Paul", liste.Single().PreteA); } } [Fact] public async Task Le_catalogue_ne_signale_plus_rien_une_fois_le_livre_rendu() { var livreId = await LivreAsync(); var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!; await _prets.RendreAsync(pret.Id, new RetourPret()); var livre = await _catalogue.ObtenirAsync(livreId, "mathieu"); Assert.Null(livre!.PreteA); Assert.False(livre.EstPrete); } /// /// Choix assumé : supprimer un livre emporte ses prêts. Un historique orphelin — « quelqu'un /// a emprunté quelque chose » — ne se lit plus. La cascade était déjà déclarée sur la relation, /// ce test la fige pour qu'un changement de comportement ne passe pas inaperçu. /// [Fact] public async Task Supprimer_un_livre_emporte_son_historique_de_prets() { var livreId = await LivreAsync(); var pret = (await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Paul" })).Pret!; await _prets.RendreAsync(pret.Id, new RetourPret()); await _prets.PreterAsync(livreId, new EnregistrementPret { Emprunteur = "Marie" }); Assert.True(await _catalogue.SupprimerAsync(livreId)); Assert.Empty(await _db.Prets.ToListAsync()); Assert.Empty(await _prets.EnCoursAsync()); } public void Dispose() { _db.Dispose(); _connexion.Dispose(); GC.SuppressFinalize(this); } }