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>
654 lines
25 KiB
C#
654 lines
25 KiB
C#
using MaBibli.Api.Data;
|
|
using MaBibli.Api.Services.Catalogue;
|
|
using MaBibli.Shared.Catalogue;
|
|
using MaBibli.Shared.Dtos;
|
|
using MaBibli.Shared.Entites;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace MaBibli.Tests;
|
|
|
|
/// <summary>
|
|
/// CRUD du catalogue, sur une base SQLite <b>en mémoire</b> : même moteur qu'en production,
|
|
/// aucun fichier, aucun réseau.
|
|
/// </summary>
|
|
public class ServiceCatalogueTests : IDisposable
|
|
{
|
|
private readonly SqliteConnection _connexion;
|
|
private readonly MaBibliDbContext _db;
|
|
private readonly ServiceCatalogue _service;
|
|
|
|
public ServiceCatalogueTests()
|
|
{
|
|
// La connexion doit rester ouverte : SQLite détruit une base « :memory: » à sa fermeture.
|
|
_connexion = new SqliteConnection("Data Source=:memory:");
|
|
_connexion.Open();
|
|
|
|
var options = new DbContextOptionsBuilder<MaBibliDbContext>()
|
|
.UseSqlite(_connexion)
|
|
.Options;
|
|
|
|
_db = new MaBibliDbContext(options);
|
|
_db.Database.EnsureCreated();
|
|
|
|
_service = new ServiceCatalogue(_db, new ServiceAuteurs(_db));
|
|
}
|
|
|
|
private static EnregistrementLivre Saisie(
|
|
string titre,
|
|
string? auteur = null,
|
|
string? isbn = null,
|
|
Format format = Format.Physique,
|
|
Statut? statut = null,
|
|
params string[] themes) => new()
|
|
{
|
|
Titre = titre,
|
|
Auteur = auteur,
|
|
Isbn = isbn,
|
|
Format = format,
|
|
Statut = statut,
|
|
Themes = themes.ToList(),
|
|
};
|
|
|
|
[Fact]
|
|
public async Task Les_themes_sont_communs_et_normalises()
|
|
{
|
|
var premier = (await _service.CreerAsync(
|
|
Saisie("Dune", themes: ["Science-fiction", "Dark Fantasy"]), "mathieu")).Livre!;
|
|
var second = (await _service.CreerAsync(
|
|
Saisie("Fondation", themes: ["science-fiction", "dark fantasy"]), "camille")).Livre!;
|
|
|
|
Assert.Equal(["Dark Fantasy", "Science-fiction"], premier.Themes);
|
|
Assert.Equal(["Dark Fantasy", "Science-fiction"], second.Themes);
|
|
Assert.Equal(2, await _db.Themes.CountAsync());
|
|
Assert.Equal(4, await _db.LivreThemes.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Modifier_remplace_les_themes_du_livre()
|
|
{
|
|
var livre = (await _service.CreerAsync(
|
|
Saisie("Dune", themes: ["Science-fiction", "Space opera"]), "mathieu")).Livre!;
|
|
|
|
var modifie = await _service.ModifierAsync(
|
|
livre.Id, Saisie("Dune", themes: ["space OPERA", "Aventure"]), "mathieu");
|
|
|
|
Assert.Equal(["Aventure", "Space opera"], modifie.Livre!.Themes);
|
|
Assert.Equal(2, await _db.LivreThemes.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Creer_enregistre_le_livre_et_trace_lauteur_de_la_saisie()
|
|
{
|
|
var resultat = await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
|
|
|
|
Assert.Null(resultat.Erreur);
|
|
var livre = resultat.Livre!;
|
|
Assert.True(livre.Id > 0);
|
|
Assert.Equal("Germinal", livre.Titre);
|
|
Assert.Equal("mathieu", livre.AjoutePar);
|
|
Assert.Equal(["Émile Zola"], livre.Auteurs.Select(a => a.Nom));
|
|
Assert.NotEqual(default, livre.DateAjout);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Creer_normalise_lisbn()
|
|
{
|
|
var resultat = await _service.CreerAsync(Saisie("Germinal", isbn: "978-2-253-00422-6"), "mathieu");
|
|
|
|
Assert.Equal("9782253004226", resultat.Livre!.Isbn);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Creer_conserve_lurl_de_la_notice_source()
|
|
{
|
|
var resultat = await _service.CreerAsync(
|
|
new EnregistrementLivre
|
|
{
|
|
Titre = "Germinal",
|
|
UrlNotice = "https://catalogue.bnf.fr/ark:/12148/cb34676750b",
|
|
},
|
|
"mathieu");
|
|
|
|
Assert.Equal(
|
|
"https://catalogue.bnf.fr/ark:/12148/cb34676750b",
|
|
resultat.Livre!.UrlNotice);
|
|
Assert.Equal(
|
|
resultat.Livre.UrlNotice,
|
|
(await _db.Livres.SingleAsync()).UrlNotice);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Le nombre de pages est facultatif, et son absence est une <b>vraie</b> absence.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// ⚠️ <c>0</c> est refusé plutôt qu'enregistré : la colonne est nullable précisément pour
|
|
/// que « je ne sais pas » ait sa propre valeur, et un zéro stocké se lirait comme une donnée.
|
|
/// </remarks>
|
|
[Fact]
|
|
public async Task Creer_conserve_le_nombre_de_pages_ou_son_absence()
|
|
{
|
|
var avec = await _service.CreerAsync(
|
|
new EnregistrementLivre { Titre = "Germinal", NombrePages = 503 }, "mathieu");
|
|
Assert.Equal(503, avec.Livre!.NombrePages);
|
|
|
|
var sans = await _service.CreerAsync(
|
|
new EnregistrementLivre { Titre = "Nana" }, "mathieu");
|
|
Assert.Null(sans.Livre!.NombrePages);
|
|
Assert.Null((await _db.Livres.SingleAsync(l => l.Titre == "Nana")).NombrePages);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Creer_refuse_un_nombre_de_pages_nul_ou_negatif()
|
|
{
|
|
var resultat = await _service.CreerAsync(
|
|
new EnregistrementLivre { Titre = "Germinal", NombrePages = 0 }, "mathieu");
|
|
|
|
Assert.NotNull(resultat.Erreur);
|
|
Assert.Empty(_db.Livres);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Modifier_peut_effacer_le_nombre_de_pages()
|
|
{
|
|
var livre = (await _service.CreerAsync(
|
|
new EnregistrementLivre { Titre = "Germinal", NombrePages = 503 }, "mathieu")).Livre!;
|
|
|
|
var resultat = await _service.ModifierAsync(
|
|
livre.Id, new EnregistrementLivre { Titre = "Germinal" }, "mathieu");
|
|
|
|
// Effacer le champ le remet à « inconnu » : c'est la seule façon de corriger une valeur
|
|
// proposée à tort par une notice.
|
|
Assert.Null(resultat.Livre!.NombrePages);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Creer_refuse_un_titre_vide()
|
|
{
|
|
var resultat = await _service.CreerAsync(Saisie(" "), "mathieu");
|
|
|
|
Assert.Null(resultat.Livre);
|
|
Assert.NotNull(resultat.Erreur);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Creer_refuse_un_isbn_invalide_mais_accepte_labsence_disbn()
|
|
{
|
|
var invalide = await _service.CreerAsync(Saisie("Sans titre", isbn: "1234567890123"), "mathieu");
|
|
Assert.Null(invalide.Livre);
|
|
Assert.NotNull(invalide.Erreur);
|
|
|
|
// Certains livres n'ont pas d'ISBN : l'absence doit rester parfaitement légitime.
|
|
var sansIsbn = await _service.CreerAsync(Saisie("Carnet de famille"), "mathieu");
|
|
Assert.Null(sansIsbn.Erreur);
|
|
Assert.Null(sansIsbn.Livre!.Isbn);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Le_catalogue_est_unique_physiques_et_numeriques_cohabitent()
|
|
{
|
|
await _service.CreerAsync(Saisie("Papier", format: Format.Physique), "mathieu");
|
|
await _service.CreerAsync(Saisie("Epub", format: Format.Numerique), "mathieu");
|
|
|
|
var tout = await _service.ListerAsync(new CritereLivres(), "mathieu");
|
|
Assert.Equal(2, tout.Count);
|
|
|
|
var numeriques = await _service.ListerAsync(new CritereLivres { Format = Format.Numerique }, "mathieu");
|
|
Assert.Equal(["Epub"], numeriques.Select(l => l.Titre));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task La_recherche_traverse_bien_SQLite()
|
|
{
|
|
await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
|
|
await _service.CreerAsync(Saisie("Le Horla", "Guy de Maupassant"), "mathieu");
|
|
|
|
var parAuteur = await _service.ListerAsync(new CritereLivres { Recherche = "maupassant" }, "mathieu");
|
|
Assert.Equal(["Le Horla"], parAuteur.Select(l => l.Titre));
|
|
|
|
var parTitre = await _service.ListerAsync(new CritereLivres { Recherche = "GERMI" }, "mathieu");
|
|
Assert.Equal(["Germinal"], parTitre.Select(l => l.Titre));
|
|
}
|
|
|
|
/// <summary>
|
|
/// La recherche sans accents doit traverser SQLite, pas seulement LINQ-to-Objects : c'est là
|
|
/// que la colonne normalisée gagne son existence.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData("emile")]
|
|
[InlineData("EMILE")]
|
|
[InlineData("Émile Zola")]
|
|
public async Task La_recherche_sans_accents_traverse_bien_SQLite(string terme)
|
|
{
|
|
await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
|
|
await _service.CreerAsync(Saisie("Le Horla", "Guy de Maupassant"), "mathieu");
|
|
|
|
var resultat = await _service.ListerAsync(new CritereLivres { Recherche = terme }, "mathieu");
|
|
|
|
Assert.Equal(["Germinal"], resultat.Select(l => l.Titre));
|
|
}
|
|
|
|
/// <summary>
|
|
/// <b>Invariant central</b> : la bibliothèque est commune. Des livres saisis par des comptes
|
|
/// différents doivent tous ressortir, quelle que soit la personne qui consulte.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task La_liste_renvoie_les_livres_de_tous_les_utilisateurs()
|
|
{
|
|
await _service.CreerAsync(Saisie("Livre de Mathieu"), "mathieu");
|
|
await _service.CreerAsync(Saisie("Livre de Camille"), "camille");
|
|
await _service.CreerAsync(Saisie("Livre anonyme"), null);
|
|
|
|
var tout = await _service.ListerAsync(new CritereLivres(), "mathieu");
|
|
|
|
Assert.Equal(3, tout.Count);
|
|
Assert.Contains(tout, l => l.AjoutePar == "mathieu");
|
|
Assert.Contains(tout, l => l.AjoutePar == "camille");
|
|
Assert.Contains(tout, l => l.AjoutePar is null);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Modifier_ne_reecrit_ni_AjoutePar_ni_DateAjout()
|
|
{
|
|
var cree = (await _service.CreerAsync(Saisie("Titre initial", "Auteur Initial"), "camille")).Livre!;
|
|
|
|
var modifie = await _service.ModifierAsync(cree.Id, new EnregistrementLivre
|
|
{
|
|
Titre = "Titre corrigé",
|
|
Auteur = "Auteur Corrigé",
|
|
Format = Format.Numerique,
|
|
Statut = Statut.Lu,
|
|
}, "camille");
|
|
|
|
Assert.Null(modifie.Erreur);
|
|
Assert.Equal("Titre corrigé", modifie.Livre!.Titre);
|
|
Assert.Equal(Format.Numerique, modifie.Livre.Format);
|
|
Assert.Equal(["Auteur Corrigé"], modifie.Livre.Auteurs.Select(a => a.Nom));
|
|
|
|
// La trace de la saisie d'origine survit à l'édition, y compris par quelqu'un d'autre.
|
|
Assert.Equal("camille", modifie.Livre.AjoutePar);
|
|
Assert.Equal(cree.DateAjout, modifie.Livre.DateAjout);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Modifier_un_livre_inexistant_est_signale_comme_introuvable()
|
|
{
|
|
var resultat = await _service.ModifierAsync(4242, Saisie("Fantôme"), "mathieu");
|
|
|
|
Assert.True(resultat.EstIntrouvable);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Changer_le_statut_ne_touche_que_le_statut()
|
|
{
|
|
var cree = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
|
|
|
|
var apres = await _service.ChangerStatutAsync(cree.Id, Statut.EnCours, "mathieu");
|
|
|
|
Assert.NotNull(apres);
|
|
Assert.Equal(Statut.EnCours, apres!.Statut);
|
|
Assert.Equal(cree.Titre, apres.Titre);
|
|
Assert.Equal(cree.Auteurs.Select(a => a.Nom), apres.Auteurs.Select(a => a.Nom));
|
|
Assert.Equal(cree.AjoutePar, apres.AjoutePar);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <b>La décision de cette phase, vérifiée de bout en bout.</b> Un seul livre, deux personnes,
|
|
/// deux statuts qui ne se marchent pas dessus.
|
|
/// </summary>
|
|
[Fact]
|
|
public async Task Deux_utilisateurs_ont_des_statuts_differents_sur_le_meme_livre()
|
|
{
|
|
var livre = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
|
|
|
|
await _service.ChangerStatutAsync(livre.Id, Statut.Lu, "mathieu");
|
|
await _service.ChangerStatutAsync(livre.Id, Statut.EnCours, "camille");
|
|
|
|
Assert.Equal(Statut.Lu, (await _service.ObtenirAsync(livre.Id, "mathieu"))!.Statut);
|
|
Assert.Equal(Statut.EnCours, (await _service.ObtenirAsync(livre.Id, "camille"))!.Statut);
|
|
|
|
// Une troisième personne n'a rien posé : pour elle le livre est « non commencé ».
|
|
Assert.Null((await _service.ObtenirAsync(livre.Id, "claire"))!.Statut);
|
|
|
|
// Une seule ligne par personne : rien ne s'empile à force de changer d'avis.
|
|
Assert.Equal(2, await _db.StatutsLecture.CountAsync(s => s.LivreId == livre.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_livre_cree_sans_statut_nen_ecrit_aucun()
|
|
{
|
|
var livre = (await _service.CreerAsync(Saisie("Germinal"), "mathieu")).Livre!;
|
|
|
|
Assert.Null((await _service.ObtenirAsync(livre.Id, "mathieu"))!.Statut);
|
|
Assert.Empty(_db.StatutsLecture);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_statut_nul_remet_le_livre_a_non_commence()
|
|
{
|
|
var livre = (await _service.CreerAsync(Saisie("Germinal", statut: Statut.Lu), "mathieu")).Livre!;
|
|
Assert.Equal(Statut.Lu, livre.Statut);
|
|
|
|
var apres = await _service.ChangerStatutAsync(livre.Id, null, "mathieu");
|
|
|
|
Assert.Null(apres!.Statut);
|
|
Assert.Empty(_db.StatutsLecture);
|
|
}
|
|
|
|
/// <summary>Éditer un livre pose MON statut, jamais celui de la personne qui l'a saisi.</summary>
|
|
[Fact]
|
|
public async Task Modifier_un_livre_ne_touche_pas_au_statut_des_autres()
|
|
{
|
|
var livre = (await _service.CreerAsync(Saisie("Germinal", statut: Statut.Lu), "mathieu")).Livre!;
|
|
|
|
await _service.ModifierAsync(
|
|
livre.Id, Saisie("Germinal", statut: Statut.EnCours), "camille");
|
|
|
|
Assert.Equal(Statut.Lu, (await _service.ObtenirAsync(livre.Id, "mathieu"))!.Statut);
|
|
Assert.Equal(Statut.EnCours, (await _service.ObtenirAsync(livre.Id, "camille"))!.Statut);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Changer_le_statut_dun_livre_inexistant_renvoie_null()
|
|
=> Assert.Null(await _service.ChangerStatutAsync(4242, Statut.Lu, "mathieu"));
|
|
|
|
[Fact]
|
|
public async Task Sans_identite_aucun_statut_nest_ecrit()
|
|
{
|
|
var livre = (await _service.CreerAsync(Saisie("Anonyme", statut: Statut.Lu), null)).Livre!;
|
|
|
|
Assert.Null(livre.Statut);
|
|
Assert.Empty(_db.StatutsLecture);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_livre_peut_avoir_plusieurs_auteurs_dans_lordre()
|
|
{
|
|
var livre = (await _service.CreerAsync(
|
|
Saisie("Introduction to Algorithms",
|
|
"Thomas H. Cormen; Charles E. Leiserson; Ronald L. Rivest; Clifford Stein"),
|
|
"mathieu")).Livre!;
|
|
|
|
Assert.Equal(
|
|
["Thomas H. Cormen", "Charles E. Leiserson", "Ronald L. Rivest", "Clifford Stein"],
|
|
livre.Auteurs.Select(a => a.Nom));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Les_variantes_sures_dun_auteur_partagent_la_meme_fiche()
|
|
{
|
|
var a = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
|
|
var b = (await _service.CreerAsync(Saisie("Nana", "Zola, Emile"), "mathieu")).Livre!;
|
|
var c = (await _service.CreerAsync(Saisie("La Curée", "E. Zola"), "mathieu")).Livre!;
|
|
|
|
var id = a.Auteurs.Single().Id;
|
|
Assert.Equal(id, b.Auteurs.Single().Id);
|
|
Assert.Equal(id, c.Auteurs.Single().Id);
|
|
Assert.Equal(1, await _db.Auteurs.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_patronyme_seul_ne_se_fond_pas_dans_un_nom_complet()
|
|
{
|
|
var complet = (await _service.CreerAsync(
|
|
Saisie("Pandora's Star", "Peter F. Hamilton"), "mathieu")).Livre!;
|
|
var seul = (await _service.CreerAsync(Saisie("Autre livre", "Hamilton"), "mathieu")).Livre!;
|
|
|
|
// Deux fiches distinctes : le rapprochement est ambigu, il sera proposé, pas appliqué.
|
|
Assert.NotEqual(complet.Auteurs.Single().Id, seul.Auteurs.Single().Id);
|
|
Assert.Equal(2, await _db.Auteurs.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Supprimer_retire_le_livre_du_catalogue()
|
|
{
|
|
var cree = (await _service.CreerAsync(Saisie("À jeter", "Auteur Jetable"), "mathieu")).Livre!;
|
|
|
|
Assert.True(await _service.SupprimerAsync(cree.Id));
|
|
Assert.Null(await _service.ObtenirAsync(cree.Id, "mathieu"));
|
|
Assert.Empty(await _service.ListerAsync(new CritereLivres(), "mathieu"));
|
|
|
|
// L'auteur n'a plus aucun livre : il ne doit pas rester à polluer l'écran des auteurs.
|
|
Assert.Empty(_db.Auteurs);
|
|
|
|
// Seconde suppression : plus rien à supprimer.
|
|
Assert.False(await _service.SupprimerAsync(cree.Id));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_auteur_encore_lu_par_un_autre_livre_survit_a_une_suppression()
|
|
{
|
|
var premier = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
|
|
await _service.CreerAsync(Saisie("Nana", "Émile Zola"), "mathieu");
|
|
|
|
await _service.SupprimerAsync(premier.Id);
|
|
|
|
Assert.Equal(1, await _db.Auteurs.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Obtenir_renvoie_la_fiche_complete()
|
|
{
|
|
var cree = (await _service.CreerAsync(
|
|
Saisie("Le Horla", "Guy de Maupassant", "9782080704092"), "mathieu")).Livre!;
|
|
|
|
var relu = await _service.ObtenirAsync(cree.Id, "mathieu");
|
|
|
|
Assert.NotNull(relu);
|
|
Assert.Equal("Le Horla", relu!.Titre);
|
|
Assert.Equal("9782080704092", relu.Isbn);
|
|
Assert.Equal(["Guy de Maupassant"], relu.Auteurs.Select(a => a.Nom));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_livre_sans_type_precise_reste_non_precise()
|
|
{
|
|
var livre = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
|
|
|
|
// ⚠️ Le défaut n'est PAS « Roman » : on ne remplit pas une case à la place de
|
|
// l'utilisateur, et l'interface n'affiche que ce qui a été choisi.
|
|
Assert.Equal(TypeDocument.NonPrecise, livre.TypeDocument);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Le_type_de_document_est_enregistre_puis_modifiable()
|
|
{
|
|
var saisie = Saisie("Les fourmis", "Bernard Werber");
|
|
saisie.TypeDocument = TypeDocument.BandeDessinee;
|
|
|
|
var cree = (await _service.CreerAsync(saisie, "mathieu")).Livre!;
|
|
Assert.Equal(TypeDocument.BandeDessinee, cree.TypeDocument);
|
|
|
|
saisie.TypeDocument = TypeDocument.Roman;
|
|
var modifie = (await _service.ModifierAsync(cree.Id, saisie, "mathieu")).Livre!;
|
|
|
|
Assert.Equal(TypeDocument.Roman, modifie.TypeDocument);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Le_catalogue_se_filtre_par_type_de_document()
|
|
{
|
|
var bd = Saisie("Les fourmis", "Bernard Werber");
|
|
bd.TypeDocument = TypeDocument.BandeDessinee;
|
|
await _service.CreerAsync(bd, "mathieu");
|
|
await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
|
|
|
|
var bds = await _service.ListerAsync(
|
|
new CritereLivres { TypeDocument = TypeDocument.BandeDessinee }, "mathieu");
|
|
var nonPrecises = await _service.ListerAsync(
|
|
new CritereLivres { TypeDocument = TypeDocument.NonPrecise }, "mathieu");
|
|
|
|
Assert.Equal(["Les fourmis"], bds.Select(l => l.Titre));
|
|
Assert.Equal(["Germinal"], nonPrecises.Select(l => l.Titre));
|
|
}
|
|
|
|
// ── Rôles des auteurs : ils vivent sur le LIEN, pas sur la personne ──────
|
|
|
|
[Fact]
|
|
public async Task Les_roles_sont_enregistres_par_livre_et_pas_par_personne()
|
|
{
|
|
var bd = Saisie("Le Horla", format: Format.Physique);
|
|
bd.TypeDocument = TypeDocument.BandeDessinee;
|
|
bd.Auteurs =
|
|
[
|
|
new AuteurSaisi("Guy de Maupassant", RoleAuteur.Scenario),
|
|
new AuteurSaisi("Guillaume Sorel", RoleAuteur.Dessin),
|
|
];
|
|
|
|
var premier = (await _service.CreerAsync(bd, "mathieu")).Livre!;
|
|
|
|
// Le MÊME dessinateur, scénariste ailleurs : une seule fiche d'auteur, deux rôles.
|
|
var autre = Saisie("Autre album");
|
|
autre.Auteurs = [new AuteurSaisi("Guillaume Sorel", RoleAuteur.Scenario)];
|
|
var second = (await _service.CreerAsync(autre, "mathieu", confirmerDoublon: true)).Livre!;
|
|
|
|
Assert.Equal(
|
|
[RoleAuteur.Scenario, RoleAuteur.Dessin],
|
|
premier.Auteurs.Select(a => a.Role));
|
|
|
|
Assert.Equal(RoleAuteur.Scenario, second.Auteurs.Single().Role);
|
|
Assert.Equal(2, await _db.Auteurs.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_livre_sans_role_precise_reste_non_precise()
|
|
{
|
|
var livre = (await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu")).Livre!;
|
|
|
|
Assert.Equal(RoleAuteur.NonPrecise, livre.Auteurs.Single().Role);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Le_role_suit_lauteur_meme_si_son_nom_est_ecrit_autrement()
|
|
{
|
|
var saisie = Saisie("Le Horla");
|
|
saisie.Auteurs =
|
|
[
|
|
// Variante sûre d'un auteur déjà connu : le serveur la rapproche, et le rôle doit
|
|
// suivre la fiche RÉSOLUE, pas la position dans la saisie.
|
|
new AuteurSaisi("Maupassant, Guy de", RoleAuteur.Scenario),
|
|
];
|
|
|
|
await _service.CreerAsync(Saisie("Bel-Ami", "Guy de Maupassant"), "mathieu");
|
|
var livre = (await _service.CreerAsync(saisie, "mathieu")).Livre!;
|
|
|
|
Assert.Single(await _db.Auteurs.ToListAsync());
|
|
Assert.Equal(RoleAuteur.Scenario, livre.Auteurs.Single().Role);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Modifier_un_livre_change_le_role_sans_toucher_a_lauteur()
|
|
{
|
|
var saisie = Saisie("Le Horla");
|
|
saisie.Auteurs = [new AuteurSaisi("Guillaume Sorel", RoleAuteur.Dessin)];
|
|
var cree = (await _service.CreerAsync(saisie, "mathieu")).Livre!;
|
|
|
|
saisie.Auteurs = [new AuteurSaisi("Guillaume Sorel", RoleAuteur.Couleurs)];
|
|
var modifie = (await _service.ModifierAsync(cree.Id, saisie, "mathieu")).Livre!;
|
|
|
|
Assert.Equal(RoleAuteur.Couleurs, modifie.Auteurs.Single().Role);
|
|
Assert.Single(await _db.Auteurs.ToListAsync());
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
// Doublons : un AVERTISSEMENT, jamais un refus. Posséder deux exemplaires est légitime —
|
|
// on garde le sien et on prête l'autre — et l'ISBN est facultatif, donc il ne peut pas
|
|
// servir de clé unique. Ces tests verrouillent les deux moitiés : ce qui est signalé, et
|
|
// le fait qu'une confirmation suffise toujours à passer outre.
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public async Task Le_meme_isbn_est_signale_sans_rien_ecrire()
|
|
{
|
|
await _service.CreerAsync(Saisie("Le Horla", "Guy de Maupassant", "9782080704092"), "mathieu");
|
|
|
|
var second = await _service.CreerAsync(
|
|
Saisie("Le Horla", "Guy de Maupassant", "9782080704092"), "mathieu");
|
|
|
|
Assert.Null(second.Livre);
|
|
Assert.Null(second.Erreur);
|
|
var doublons = second.Doublons!;
|
|
Assert.Contains("Le Horla", doublons.Message);
|
|
Assert.Single(doublons.Livres);
|
|
Assert.Equal(1, await _db.Livres.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Le_meme_isbn_confirme_cree_bien_un_second_exemplaire()
|
|
{
|
|
await _service.CreerAsync(Saisie("Le Horla", "Guy de Maupassant", "9782080704092"), "mathieu");
|
|
|
|
var second = await _service.CreerAsync(
|
|
Saisie("Le Horla", "Guy de Maupassant", "9782080704092"), "mathieu", confirmerDoublon: true);
|
|
|
|
Assert.Null(second.Doublons);
|
|
Assert.NotNull(second.Livre);
|
|
Assert.Equal(2, await _db.Livres.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Sans_isbn_le_titre_et_lauteur_suffisent_a_signaler()
|
|
{
|
|
await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
|
|
|
|
// Sous-titre, accents et ordre du nom diffèrent : c'est exactement ce que CleOeuvre et
|
|
// RapprochementAuteurs sont là pour absorber.
|
|
var second = await _service.CreerAsync(Saisie("Germinal : roman", "Zola, Emile"), "camille");
|
|
|
|
Assert.NotNull(second.Doublons);
|
|
Assert.Single(second.Doublons!.Livres);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_titre_identique_dun_autre_auteur_nest_pas_un_doublon()
|
|
{
|
|
await _service.CreerAsync(Saisie("Nouvelles", "Guy de Maupassant"), "mathieu");
|
|
|
|
var autre = await _service.CreerAsync(Saisie("Nouvelles", "Anton Tchekhov"), "mathieu");
|
|
|
|
Assert.Null(autre.Doublons);
|
|
Assert.NotNull(autre.Livre);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_titre_seulement_commence_pareil_nest_pas_un_doublon()
|
|
{
|
|
await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
|
|
|
|
// Le préfixe attrape le candidat en SQL, la clé d'œuvre le rejette ensuite en mémoire :
|
|
// sans cette seconde passe, tout ce qui commence par « Germinal » serait signalé.
|
|
var autre = await _service.CreerAsync(Saisie("Germinal les années noires", "Émile Zola"), "mathieu");
|
|
|
|
Assert.Null(autre.Doublons);
|
|
Assert.NotNull(autre.Livre);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Un_livre_trouve_par_son_isbn_ET_son_titre_nest_signale_quune_fois()
|
|
{
|
|
await _service.CreerAsync(Saisie("Germinal", "Émile Zola", "9782253004226"), "mathieu");
|
|
|
|
var second = await _service.CreerAsync(Saisie("Germinal", "Émile Zola", "9782253004226"), "mathieu");
|
|
|
|
Assert.Single(second.Doublons!.Livres);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Une_saisie_invalide_est_une_erreur_avant_detre_un_doublon()
|
|
{
|
|
await _service.CreerAsync(Saisie("Germinal", "Émile Zola"), "mathieu");
|
|
|
|
var invalide = await _service.CreerAsync(Saisie("Germinal", "Émile Zola", "1234567890123"), "mathieu");
|
|
|
|
Assert.NotNull(invalide.Erreur);
|
|
Assert.Null(invalide.Doublons);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_db.Dispose();
|
|
_connexion.Dispose();
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|