using MaBibli.Api.Data; using MaBibli.Api.Services.Catalogue; using MaBibli.Api.Services.Isbn; using MaBibli.Api.Services.Souhaits; using MaBibli.Shared.Catalogue; using MaBibli.Shared.Dtos; using MaBibli.Shared.Entites; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace MaBibli.Tests; /// /// Liste d'envies, sur une base SQLite en mémoire : l'unicité « une œuvre par personne » /// n'existe qu'en base, elle ne se vérifierait pas sur un faux dépôt. /// /// /// L'invariant central testé ici est la portée personnelle : deux utilisateurs ne doivent /// jamais se voir. C'est l'exact contraire du catalogue, commun au foyer — et une régression y /// serait discrète, puisque tout continuerait de « marcher » avec un seul compte. /// public class ServiceSouhaitsTests : IDisposable { private readonly SqliteConnection _connexion; private readonly MaBibliDbContext _db; private readonly ServiceSouhaits _souhaits; private readonly ServiceCatalogue _catalogue; private const string Mathieu = "mathieu"; private const string Camille = "camille"; public ServiceSouhaitsTests() { _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); _catalogue = new ServiceCatalogue(_db, new ServiceAuteurs(_db)); } private Task AjouterAsync( string titre, string? auteur = "Bernard Werber", string utilisateur = Mathieu, string? isbn = null, string? note = null) => _souhaits.AjouterAsync( new EnregistrementSouhait { Titre = titre, Auteur = auteur, Isbn = isbn, Note = note }, utilisateur); // ── « Déjà au catalogue » : on signale, on ne supprime jamais ──────────── // // Le catalogue est COMMUN, la liste d'envies PERSONNELLE : c'est ce croisement de portées // que ces tests protègent. Retirer l'envie d'un autre, comme le demandait la version // initiale de l'idée, modifierait sa liste en silence. [Fact] public async Task Une_envie_deja_au_catalogue_est_signalee_avec_sa_fiche() { var livre = (await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu)).Livre!; // Sous-titre et ordre du nom différents : ce que la clé d'œuvre et le rapprochement // d'auteurs doivent absorber. await AjouterAsync("Les fourmis : roman", "Werber, Bernard"); var souhait = (await _souhaits.ListerAsync(Mathieu)).Single(); Assert.True(souhait.Possede); Assert.Equal(livre.Id, souhait.LivreId); } [Fact] public async Task Une_envie_signalee_reste_dans_la_liste_avec_sa_note() { await AjouterAsync("Les fourmis", note: "demandé à Noël"); await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Camille); var souhait = (await _souhaits.ListerAsync(Mathieu)).Single(); // L'ajout au catalogue par QUELQU'UN D'AUTRE ne touche pas à la liste de Mathieu. Assert.True(souhait.Possede); Assert.Equal("demandé à Noël", souhait.Note); } [Fact] public async Task Un_titre_identique_dun_autre_auteur_nest_pas_signale() { await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Nouvelles", Auteur = "Guy de Maupassant" }, Mathieu); await AjouterAsync("Nouvelles", "Anton Tchekhov"); Assert.False((await _souhaits.ListerAsync(Mathieu)).Single().Possede); } [Fact] public async Task Le_meme_isbn_suffit_a_signaler_malgre_un_titre_different() { // L'ISBN désigne une édition précise : il tranche seul, et rattrape un titre saisi // autrement qu'à la source. var livre = (await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Le Horla et autres contes", Auteur = "Guy de Maupassant", Isbn = "9782080704092", }, Mathieu)).Livre!; await AjouterAsync("Le Horla", "Maupassant", isbn: "9782080704092"); var souhait = (await _souhaits.ListerAsync(Mathieu)).Single(); Assert.Equal(livre.Id, souhait.LivreId); } [Fact] public async Task Une_envie_sans_equivalent_nest_pas_signalee() { await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu); await AjouterAsync("La prophétie des abeilles"); var souhait = (await _souhaits.ListerAsync(Mathieu)).Single(); Assert.False(souhait.Possede); Assert.Null(souhait.LivreId); } [Fact] public async Task Le_signalement_apparait_des_lajout_de_lenvie() { await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu); var ajout = await AjouterAsync("Les fourmis"); // Sinon l'écran d'ajout renverrait une fiche muette, et il faudrait recharger la liste // pour apprendre qu'on possède déjà le livre. Assert.True(ajout.Souhait!.Possede); } // ── Ordre choisi à la main ─────────────────────────────────────────────── private async Task> TitresAsync(string utilisateur = Mathieu) => (await _souhaits.ListerAsync(utilisateur)).Select(s => s.Titre).ToList(); /// /// L'ordre d'affichage est celui de la liste, pas l'alphabet : classer une liste d'envies /// par titre répondait à une question que personne ne se pose. /// [Fact] public async Task Les_envies_sortent_dans_lordre_dajout_par_defaut() { await AjouterAsync("Zèbre", auteur: "Zoé Zed"); await AjouterAsync("Abricot", auteur: "Anne Alpha"); Assert.Equal(["Zèbre", "Abricot"], await TitresAsync()); } [Fact] public async Task Une_envie_nouvelle_se_pose_en_fin_de_liste() { await AjouterAsync("Premier"); await AjouterAsync("Deuxième"); await AjouterAsync("Troisième"); Assert.Equal(["Premier", "Deuxième", "Troisième"], await TitresAsync()); } [Fact] public async Task Reordonner_fixe_lordre_demande() { var a = (await AjouterAsync("Premier")).Souhait!.Id; var b = (await AjouterAsync("Deuxième")).Souhait!.Id; var c = (await AjouterAsync("Troisième")).Souhait!.Id; Assert.True(await _souhaits.ReordonnerAsync([c, a, b], Mathieu)); Assert.Equal(["Troisième", "Premier", "Deuxième"], await TitresAsync()); } /// Réenvoyer le même ordre ne doit rien bouger. [Fact] public async Task Reordonner_est_idempotent() { var a = (await AjouterAsync("Premier")).Souhait!.Id; var b = (await AjouterAsync("Deuxième")).Souhait!.Id; await _souhaits.ReordonnerAsync([b, a], Mathieu); await _souhaits.ReordonnerAsync([b, a], Mathieu); Assert.Equal(["Deuxième", "Premier"], await TitresAsync()); } /// /// ⚠️ Le cas qui perdrait des données : la liste du client est périmée (une envie ajoutée /// depuis un autre appareil n'y figure pas). L'absente doit survivre, rangée à la /// suite — mal classée vaut infiniment mieux que disparue. /// [Fact] public async Task Une_envie_absente_de_lordre_envoye_est_conservee() { var a = (await AjouterAsync("Premier")).Souhait!.Id; var b = (await AjouterAsync("Deuxième")).Souhait!.Id; await AjouterAsync("Ajoutée ailleurs"); await _souhaits.ReordonnerAsync([b, a], Mathieu); Assert.Equal(["Deuxième", "Premier", "Ajoutée ailleurs"], await TitresAsync()); } /// /// ⚠️ L'invariant de la table : réordonner ne doit pas offrir un moyen détourné de toucher /// à la liste de quelqu'un d'autre en devinant un identifiant. /// [Fact] public async Task Reordonner_ne_touche_jamais_la_liste_dun_autre() { var sienne = (await AjouterAsync("À Camille", utilisateur: Camille)).Souhait!.Id; var a = (await AjouterAsync("Premier", utilisateur: Mathieu)).Souhait!.Id; var b = (await AjouterAsync("Deuxième", utilisateur: Mathieu)).Souhait!.Id; // Mathieu glisse l'identifiant de Camille en tête de SA demande. await _souhaits.ReordonnerAsync([sienne, b, a], Mathieu); Assert.Equal(["Deuxième", "Premier"], await TitresAsync(Mathieu)); Assert.Equal(["À Camille"], await TitresAsync(Camille)); } [Fact] public async Task Reordonner_sans_identite_ne_fait_rien() { await AjouterAsync("Premier"); Assert.False(await _souhaits.ReordonnerAsync([1], null)); } /// /// Supprimer laisse des trous dans les rangs ; ils ne doivent pas déranger l'ordre, et le /// prochain ajout doit tout de même atterrir en dernier. /// [Fact] public async Task Une_suppression_ne_desordonne_pas_la_liste() { await AjouterAsync("Premier"); var b = (await AjouterAsync("Deuxième")).Souhait!.Id; await AjouterAsync("Troisième"); Assert.True(await _souhaits.SupprimerAsync(b, Mathieu)); await AjouterAsync("Quatrième"); Assert.Equal(["Premier", "Troisième", "Quatrième"], await TitresAsync()); } // ── Portée personnelle ─────────────────────────────────────────────────── [Fact] public async Task Deux_utilisateurs_ont_deux_listes_distinctes() { await AjouterAsync("Les fourmis", utilisateur: Mathieu); await AjouterAsync("Germinal", "Émile Zola", Camille); var deMathieu = await _souhaits.ListerAsync(Mathieu); var deCamille = await _souhaits.ListerAsync(Camille); Assert.Equal("Les fourmis", Assert.Single(deMathieu).Titre); Assert.Equal("Germinal", Assert.Single(deCamille).Titre); } [Fact] public async Task Deux_utilisateurs_peuvent_souhaiter_le_meme_livre() { var premier = await AjouterAsync("Les fourmis", utilisateur: Mathieu); var second = await AjouterAsync("Les fourmis", utilisateur: Camille); // L'unicité porte sur (utilisateur, œuvre) : elle ne doit pas empêcher deux personnes // d'avoir la même envie — c'est même le cas normal dans un foyer. Assert.Null(premier.Erreur); Assert.Null(second.Erreur); } [Fact] public async Task On_ne_supprime_pas_l_envie_d_un_autre() { var souhait = await AjouterAsync("Les fourmis", utilisateur: Mathieu); var supprime = await _souhaits.SupprimerAsync(souhait.Souhait!.Id, Camille); Assert.False(supprime); Assert.Single(await _souhaits.ListerAsync(Mathieu)); } [Fact] public async Task Sans_identite_il_n_y_a_ni_liste_ni_ecriture() { var ajout = await AjouterAsync("Les fourmis", utilisateur: null!); Assert.NotNull(ajout.Erreur); Assert.Empty(await _souhaits.ListerAsync(null)); } // ── Cloisonnement d'avec le catalogue ──────────────────────────────────── /// /// L'invariant qui justifie l'entité séparée : une envie n'entre ni dans le catalogue, ni /// dans les compteurs, ni dans la table des auteurs. /// [Fact] public async Task Une_envie_n_apparait_pas_dans_le_catalogue() { await AjouterAsync("La prophétie des abeilles", "Bernard Werber"); var catalogue = await _catalogue.ListerAsync(new CritereLivres(), Mathieu); Assert.Empty(catalogue); } [Fact] public async Task Une_envie_ne_cree_pas_de_fiche_auteur() { await AjouterAsync("La prophétie des abeilles", "Bernard Werber"); // Sinon « Bernard Werber » apparaîtrait sur l'écran des auteurs avec « 0 livre », // et trahirait au passage la liste d'envies de son propriétaire. Assert.Empty(await new ServiceAuteurs(_db).ListerAsync()); } [Fact] public async Task Une_envie_recherchee_dans_le_catalogue_ne_remonte_pas() { await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu); await AjouterAsync("La prophétie des abeilles", "Bernard Werber"); var trouves = await _catalogue.ListerAsync( new CritereLivres { Recherche = "Werber" }, Mathieu); Assert.Equal("Les fourmis", Assert.Single(trouves).Titre); } // ── Validation et doublons ─────────────────────────────────────────────── [Fact] public async Task Le_titre_est_obligatoire() { var resultat = await AjouterAsync(" "); Assert.NotNull(resultat.Erreur); } [Fact] public async Task La_meme_oeuvre_ne_s_ajoute_pas_deux_fois() { await AjouterAsync("Les thanatonautes"); // Variante d'écriture du même titre : c'est exactement ce que produit un double clic // sur deux lignes de bibliographie qui n'ont pas été regroupées côté BnF. var doublon = await AjouterAsync("Les thanatonautes : roman"); Assert.NotNull(doublon.Erreur); Assert.Single(await _souhaits.ListerAsync(Mathieu)); } [Fact] public async Task Deux_auteurs_peuvent_avoir_une_oeuvre_de_meme_titre() { var premier = await AjouterAsync("Nouvelles", "Bernard Werber"); var second = await AjouterAsync("Nouvelles", "Émile Zola"); Assert.Null(premier.Erreur); Assert.Null(second.Erreur); Assert.Equal(2, (await _souhaits.ListerAsync(Mathieu)).Count); } [Fact] public async Task Un_ISBN_invalide_est_refuse() { var resultat = await AjouterAsync("Les fourmis", isbn: "1234567890123"); Assert.NotNull(resultat.Erreur); } [Fact] public async Task L_ISBN_reste_facultatif() { var resultat = await AjouterAsync("Les fourmis", isbn: null); Assert.Null(resultat.Erreur); Assert.Null(resultat.Souhait!.Isbn); } /// /// ⚠️ Contrat changé le 2026-08-18 : la liste était classée par auteur puis titre, /// elle suit désormais le rang choisi par son propriétaire. L'alphabet répondait à /// une question que personne ne se pose devant une liste d'envies ; « qu'est-ce que je veux /// le plus ? » en est une. /// /// L'ancien ordre n'est pas perdu pour autant : la migration RangDesEnvies le /// reconduit dans les rangs initiaux, pour que les listes existantes ne se réordonnent pas /// toutes seules. C'est MigrationRangDesEnviesTests qui le vérifie. /// /// [Fact] public async Task La_liste_ne_se_classe_plus_par_ordre_alphabetique() { await AjouterAsync("Les fourmis", "Bernard Werber"); await AjouterAsync("Sans auteur", null); await AjouterAsync("Germinal", "Émile Zola"); await AjouterAsync("Demain les chats", "Bernard Werber"); Assert.Equal( ["Les fourmis", "Sans auteur", "Germinal", "Demain les chats"], await TitresAsync()); } [Fact] public async Task Une_envie_supprimee_disparait() { var souhait = await AjouterAsync("Les fourmis"); Assert.True(await _souhaits.SupprimerAsync(souhait.Souhait!.Id, Mathieu)); Assert.Empty(await _souhaits.ListerAsync(Mathieu)); } // ── Bibliographie : confrontation à l'étagère ──────────────────────────── /// /// Client BnF simulé : ces tests portent sur le rapprochement, pas sur le réseau. /// private sealed class BnfBibliographieFake(string xml) : IBnfClient { public Task<(IReadOnlyList, string?)> RechercherAsync( string isbn, string? urlCouverture, CancellationToken ct = default) => Task.FromResult(((IReadOnlyList)[], (string?)null)); public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync( string auteur, CancellationToken ct = default) => Task.FromResult((BnfBibliographieParser.Parser(xml, auteur), (string?)null)); /// Les périodiques n'interviennent pas dans la bibliographie par auteur. public Task<(PeriodiqueDetecte?, string?)> RechercherPeriodiqueAsync( string issn, CancellationToken ct = default) => Task.FromResult(((PeriodiqueDetecte?)null, (string?)null)); /// Recherche par titre : couverte par ses propres tests. public Task<(IReadOnlyList, string?)> RechercherParTitreAsync( string titre, string? auteur, CancellationToken ct = default) => Task.FromResult(((IReadOnlyList)[], (string?)null)); } /// /// Client BnF qui ne répond pas : c'est le cas « Robert A. Harper » remonté en usage. /// private sealed class BnfMuetFake(EtatSourceBibliographie etat, string message) : IBnfClient { public Task<(IReadOnlyList, string?)> RechercherAsync( string isbn, string? urlCouverture, CancellationToken ct = default) => Task.FromResult(((IReadOnlyList)[], (string?)null)); public Task<(ResultatBibliographie, string?)> RechercherParAuteurAsync( string auteur, CancellationToken ct = default) => Task.FromResult((new ResultatBibliographie { Etat = etat }, (string?)message)); public Task<(PeriodiqueDetecte?, string?)> RechercherPeriodiqueAsync( string issn, CancellationToken ct = default) => Task.FromResult(((PeriodiqueDetecte?)null, (string?)null)); public Task<(IReadOnlyList, string?)> RechercherParTitreAsync( string titre, string? auteur, CancellationToken ct = default) => Task.FromResult(((IReadOnlyList)[], (string?)null)); } /// /// ⚠️ Le défaut que ce lot corrige. Une BnF muette rend une liste vide, exactement /// comme un auteur réellement absent de son catalogue. Sans SourceMuette, l'écran /// affirmait « la BnF ne connaît aucun livre de cet auteur » à propos d'une question qu'il /// n'avait pas pu poser. /// [Theory] [InlineData(EtatSourceBibliographie.DelaiDepasse)] [InlineData(EtatSourceBibliographie.Injoignable)] [InlineData(EtatSourceBibliographie.ReponseIllisible)] public async Task Une_BnF_muette_ne_se_confond_pas_avec_un_auteur_sans_oeuvre( EtatSourceBibliographie etat) { var biblio = await BibliographieAsync(new BnfMuetFake(etat, "La BnF n'a pas répondu.")); Assert.Empty(biblio.Oeuvres); Assert.True(biblio.SourceMuette); Assert.Equal(etat, biblio.Etat); Assert.NotNull(biblio.Avertissement); } /// /// Le pendant : la BnF a répondu, elle ne connaît simplement rien. Là, et là /// seulement, l'écran a le droit d'expliquer l'absence. /// [Fact] public async Task Une_BnF_qui_repond_sans_rien_connaitre_nest_pas_muette() { var biblio = await BibliographieAsync(new BnfBibliographieFake(ReponseVide)); Assert.Empty(biblio.Oeuvres); Assert.False(biblio.SourceMuette); Assert.Equal(EtatSourceBibliographie.Ok, biblio.Etat); Assert.Null(biblio.Avertissement); } /// Une bibliographie qui a abouti n'est jamais signalée comme muette. [Fact] public async Task Une_bibliographie_obtenue_nest_pas_muette() { var biblio = await BibliographieAsync( new BnfBibliographieFake(Fixture.Lire("bnf-auteur-werber.xml"))); Assert.NotEmpty(biblio.Oeuvres); Assert.False(biblio.SourceMuette); } private const string ReponseVide = """ 0 """; /// Bibliographie de Werber avec le client BnF fourni : seule la source varie. /// /// Bibliographie de Werber avec le client BnF fourni : seule la source varie. /// L'auteur doit exister au catalogue, une bibliographie partant toujours d'un auteur à soi. /// private async Task BibliographieAsync(IBnfClient bnf) { await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu); var auteurs = new ServiceAuteurs(_db); var service = new ServiceBibliographie(_db, bnf, auteurs); var werber = (await auteurs.ListerAsync()).Single(a => a.Nom.Contains("Werber")); return (await service.ObtenirAsync(werber.Id, Mathieu))!; } private async Task BibliographieWerberAsync(string utilisateur = Mathieu) { var auteurs = new ServiceAuteurs(_db); var service = new ServiceBibliographie( _db, new BnfBibliographieFake(Fixture.Lire("bnf-auteur-werber.xml")), auteurs); var werber = (await auteurs.ListerAsync()).Single(a => a.Nom.Contains("Werber")); return (await service.ObtenirAsync(werber.Id, utilisateur))!; } [Fact] public async Task La_bibliographie_grise_les_livres_deja_possedes() { // Titre saisi sans le sous-titre « : roman » que porte la notice BnF : c'est le cas // courant, et c'est précisément ce que la clé d'œuvre doit rattraper. await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les thanatonautes", Auteur = "Bernard Werber" }, Mathieu); var biblio = await BibliographieWerberAsync(); var oeuvre = biblio.Oeuvres.Single( o => o.Titre.StartsWith("Les thanatonautes", StringComparison.OrdinalIgnoreCase)); Assert.True(oeuvre.Possede); Assert.NotNull(oeuvre.LivreId); // Les autres restent à découvrir : posséder un livre n'en grise pas d'autres. Assert.Contains(biblio.Oeuvres, o => o.ADecouvrir); } [Fact] public async Task La_bibliographie_signale_ce_qui_est_deja_souhaite() { await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu); var avant = await BibliographieWerberAsync(); var cible = avant.Oeuvres.First(o => o.ADecouvrir); await AjouterAsync(cible.Titre, "Bernard Werber"); var apres = await BibliographieWerberAsync(); var revue = apres.Oeuvres.Single(o => o.Titre == cible.Titre); Assert.True(revue.Souhaite); Assert.False(revue.ADecouvrir); } /// /// « Possédé » vaut pour le foyer, « souhaité » seulement pour l'appelant : c'est ce qui /// permet d'offrir un livre à quelqu'un sans qu'il le voie venir. /// [Fact] public async Task Le_souhait_d_un_autre_n_apparait_pas_dans_ma_bibliographie() { await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu); var biblio = await BibliographieWerberAsync(Mathieu); var cible = biblio.Oeuvres.First(o => o.ADecouvrir); await AjouterAsync(cible.Titre, "Bernard Werber", Camille); var vueDeMathieu = await BibliographieWerberAsync(Mathieu); var vueDeCamille = await BibliographieWerberAsync(Camille); Assert.False(vueDeMathieu.Oeuvres.Single(o => o.Titre == cible.Titre).Souhaite); Assert.True(vueDeCamille.Oeuvres.Single(o => o.Titre == cible.Titre).Souhaite); } [Fact] public async Task Un_auteur_inconnu_n_a_pas_de_bibliographie() { var service = new ServiceBibliographie( _db, new BnfBibliographieFake(""), new ServiceAuteurs(_db)); Assert.Null(await service.ObtenirAsync(4242, Mathieu)); } /// /// Le livre d'un homonyme ne doit pas griser l'œuvre d'un autre : la comparaison se fait /// à auteur donné, pas sur tout le catalogue. /// [Fact] public async Task Un_livre_d_un_autre_auteur_ne_grise_rien() { await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu); // Un homonyme parfait, mais signé de quelqu'un d'autre. await _catalogue.CreerAsync( new EnregistrementLivre { Titre = "Les thanatonautes", Auteur = "Éric Corbeyran" }, Mathieu); var biblio = await BibliographieWerberAsync(); Assert.False( biblio.Oeuvres .Single(o => o.Titre.StartsWith("Les thanatonautes", StringComparison.OrdinalIgnoreCase)) .Possede); } // ── Éditer une envie ──────────────────────────────────────────────────── // // La liste est PERSONNELLE : c'est le seul invariant que ces tests protègent vraiment. // Un 403 dirait « elle existe, mais elle n'est pas à vous » — exactement ce qu'on refuse // de laisser deviner. [Fact] public async Task Editer_une_envie_recalcule_la_cle_doeuvre_et_lauteur() { var envie = (await AjouterAsync("Les fourmis")).Souhait!; var resultat = await _souhaits.ModifierAsync( envie.Id, new EnregistrementSouhait { Titre = "L'Œuvre : roman", Auteur = "Zola, Émile" }, Mathieu); Assert.Null(resultat.Erreur); var ligne = _db.LivresSouhaites.Single(); // Clé d'ŒUVRE : le sous-titre est coupé, la ligature développée, les accents à plat. Assert.Equal("l oeuvre", ligne.TitreNormalise); // Sans ce recalcul, le rapprochement « déjà au catalogue » se ferait sur l'ancien nom. Assert.Equal("emile zola", ligne.AuteurNormalise); } [Fact] public async Task Editer_une_envie_ne_touche_pas_a_son_rang() { await AjouterAsync("Première"); var seconde = (await AjouterAsync("Seconde")).Souhait!; await _souhaits.ModifierAsync( seconde.Id, new EnregistrementSouhait { Titre = "Seconde, corrigée" }, Mathieu); Assert.Equal(1, _db.LivresSouhaites.Single(s => s.Id == seconde.Id).Rang); } /// /// Une correction ne doit pas effacer la couverture : aucun écran n'offre de champ pour la /// saisir, un remplacement inconditionnel la perdrait donc à la première faute de frappe /// corrigée. /// [Fact] public async Task Editer_une_envie_conserve_sa_couverture() { var envie = (await _souhaits.AjouterAsync( new EnregistrementSouhait { Titre = "Les fourmis", CoverUrl = "https://exemple.test/fourmis.jpg", }, Mathieu)).Souhait!; await _souhaits.ModifierAsync( envie.Id, new EnregistrementSouhait { Titre = "Les fourmis : roman" }, Mathieu); Assert.Equal("https://exemple.test/fourmis.jpg", _db.LivresSouhaites.Single().CoverUrl); } [Fact] public async Task Editer_lenvie_dun_autre_est_introuvable_et_ne_change_rien() { var sienne = (await AjouterAsync("Les fourmis", utilisateur: Camille)).Souhait!; var resultat = await _souhaits.ModifierAsync( sienne.Id, new EnregistrementSouhait { Titre = "Détourné" }, Mathieu); // ⚠️ Introuvable, et non « refusé » : l'inexistence et l'appartenance à autrui doivent // rester indiscernables de l'extérieur. Assert.True(resultat.NExistePas); Assert.Null(resultat.Erreur); Assert.Equal("Les fourmis", _db.LivresSouhaites.Single().Titre); } [Fact] public async Task Editer_une_envie_inexistante_est_introuvable() { var resultat = await _souhaits.ModifierAsync( 404, new EnregistrementSouhait { Titre = "Fantôme" }, Mathieu); Assert.True(resultat.NExistePas); } /// /// Le cas que l'ajout ne connaît pas : renommer une envie en une autre déjà présente. /// L'unicité (Utilisateur, TitreNormalise, AuteurNormalise) l'interdit — il faut répondre /// en français, pas laisser remonter « UNIQUE constraint failed ». /// [Fact] public async Task Editer_vers_une_envie_deja_presente_est_refuse_lisiblement() { await AjouterAsync("Les fourmis"); var seconde = (await AjouterAsync("Les thanatonautes")).Souhait!; var resultat = await _souhaits.ModifierAsync( seconde.Id, new EnregistrementSouhait { Titre = "Les fourmis : roman", Auteur = "Bernard Werber" }, Mathieu); Assert.False(resultat.NExistePas); Assert.NotNull(resultat.Erreur); Assert.Contains("déjà dans votre liste", resultat.Erreur); Assert.DoesNotContain("UNIQUE", resultat.Erreur, StringComparison.OrdinalIgnoreCase); // Et rien n'a bougé en base : le refus est un refus, pas une écriture partielle. Assert.Equal(2, _db.LivresSouhaites.Count()); } /// /// Deux personnes peuvent parfaitement viser la même œuvre : l'utilisateur ouvre l'index /// unique, il n'est pas seulement un filtre de lecture. /// [Fact] public async Task Editer_vers_lenvie_dun_autre_utilisateur_est_permis() { await AjouterAsync("Les fourmis", utilisateur: Camille); var sienne = (await AjouterAsync("Les thanatonautes")).Souhait!; var resultat = await _souhaits.ModifierAsync( sienne.Id, new EnregistrementSouhait { Titre = "Les fourmis", Auteur = "Bernard Werber" }, Mathieu); Assert.Null(resultat.Erreur); Assert.Equal(2, _db.LivresSouhaites.Count()); } [Fact] public async Task Editer_avec_un_isbn_invalide_est_refuse() { var envie = (await AjouterAsync("Les fourmis")).Souhait!; var resultat = await _souhaits.ModifierAsync( envie.Id, new EnregistrementSouhait { Titre = "Les fourmis", Isbn = "123" }, Mathieu); Assert.NotNull(resultat.Erreur); Assert.Null(_db.LivresSouhaites.Single().Isbn); } public void Dispose() { _db.Dispose(); _connexion.Dispose(); GC.SuppressFinalize(this); } }