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);
}
}