Signaler les doublons à l'ajout d'un livre, sans jamais les refuser

Rien n'empêchait de rescanner un livre déjà catalogué. Plutôt qu'un index
unique — qui interdirait le second exemplaire, cas parfaitement légitime —
la création répond 409 avec les fiches semblables, et « confirmerDoublon »
enregistre la même saisie.

Deux critères, dont aucun n'est une clé : ISBN identique, ou clé d'œuvre
et auteur communs. La clé d'œuvre étant un préfixe de TitreNormalise, SQL
dégrossit sur la colonne indexée et l'égalité exacte se vérifie ensuite en
mémoire.

Vérifié en exécution : avertissement, retour au formulaire intact, et ajout
confirmé créant bien un second exemplaire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mathieu
2026-08-19 21:11:39 +02:00
co-authored by Claude Opus 5
parent 278d6579f7
commit 383ed43227
10 changed files with 600 additions and 69 deletions
+94
View File
@@ -348,6 +348,100 @@ public class ServiceCatalogueTests : IDisposable
Assert.Equal(["Guy de Maupassant"], relu.Auteurs.Select(a => a.Nom));
}
// ─────────────────────────────────────────────────────────────────────────
// 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();