Files
mabibli/MaBibli.Tests/GroupementCatalogueTests.cs
Mathieu LimonierandClaude Opus 5 6a6d745af4 MaBibli 1.0.0
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>
2026-08-22 22:36:16 +02:00

250 lines
8.9 KiB
C#

using MaBibli.Client.Services;
using MaBibli.Shared.Dtos;
namespace MaBibli.Tests;
/// <summary>
/// Regroupement des tomes d'une série au catalogue (2026-08-22).
/// </summary>
/// <remarks>
/// L'écran n'est pas éprouvable ici, mais la règle l'est : ces tests portent sur ce qui a été
/// <b>décidé</b> — la place du bloc, l'ordre des tomes à l'intérieur, et le fait que rien ne
/// disparaisse ni ne se duplique.
/// </remarks>
public class GroupementCatalogueTests
{
private static LivreDto Livre(int id, string titre) =>
new()
{
Id = id,
Titre = titre,
Format = MaBibli.Shared.Entites.Format.Physique,
DateAjout = DateTime.UtcNow,
};
private static SerieDto Serie(int id, string titre, params (int Position, int LivreId)[] tomes) =>
SousSerie(id, titre, null, 0, tomes);
private static SerieDto SousSerie(
int id, string titre, int? parenteId, int position,
params (int Position, int LivreId)[] tomes) =>
new()
{
Id = id,
Titre = titre,
SerieParenteId = parenteId,
Position = position,
Elements =
[
.. tomes.Select(t => new ElementSerieDto
{
Id = t.LivreId,
Position = t.Position,
Titre = $"tome {t.Position}",
LivreId = t.LivreId,
}),
],
};
/// <summary>
/// ⚠️ Le cas qui a tranché la décision : le bloc se pose à la place du PREMIER tome, pour
/// que l'ordre général du catalogue ne bouge pas.
/// </summary>
[Fact]
public void Le_bloc_prend_la_place_de_son_premier_tome()
{
List<LivreDto> catalogue =
[
Livre(1, "Dracula"),
Livre(2, "L'Éclat de cristal"),
Livre(3, "Dune"),
Livre(4, "Terre natale"),
];
var entrees = GroupementCatalogue.Grouper(
catalogue, [Serie(9, "La Légende de Drizzt", (1, 2), (0, 4))]);
Assert.Collection(
entrees,
e => Assert.Equal("Dracula", Assert.Single(e.Livres).Titre),
e =>
{
Assert.Equal("La Légende de Drizzt", e.Serie!.Titre);
// À l'intérieur, l'ordre est celui de LECTURE, pas celui du catalogue.
Assert.Equal(["Terre natale", "L'Éclat de cristal"], e.Livres.Select(l => l.Titre));
},
e => Assert.Equal("Dune", Assert.Single(e.Livres).Titre));
}
/// <summary>Aucun livre ne disparaît, aucun n'apparaît deux fois : c'est la règle du catalogue.</summary>
[Fact]
public void Le_regroupement_ne_perd_ni_ne_duplique_aucun_livre()
{
List<LivreDto> catalogue = [Livre(1, "A"), Livre(2, "B"), Livre(3, "C")];
// Le livre 2 est rattaché à DEUX séries — le modèle l'autorise.
var entrees = GroupementCatalogue.Grouper(
catalogue, [Serie(1, "Zeta", (0, 2)), Serie(2, "Alpha", (0, 2), (1, 3))]);
Assert.Equal([1, 2, 3], entrees.SelectMany(e => e.Livres).Select(l => l.Id).Order());
}
/// <summary>
/// ⚠️ La série retenue est la première par ordre alphabétique, et non celle que l'API a
/// rendue en premier : sans cela, le bloc changerait de nom d'un chargement à l'autre.
/// </summary>
[Fact]
public void Un_livre_de_deux_series_est_range_sous_la_premiere_alphabetiquement()
{
var entrees = GroupementCatalogue.Grouper(
[Livre(2, "B")], [Serie(1, "Zeta", (0, 2)), Serie(2, "Alpha", (0, 2))]);
Assert.Equal("Alpha", Assert.Single(entrees).Serie!.Titre);
}
/// <summary>Sans série connue — hors-ligne sans instantané, ou groupement défait — la liste est plate.</summary>
[Fact]
public void Sans_series_la_liste_reste_celle_du_catalogue()
{
List<LivreDto> catalogue = [Livre(1, "A"), Livre(2, "B")];
var entrees = GroupementCatalogue.Grouper(catalogue, null);
Assert.All(entrees, e => Assert.Null(e.Serie));
Assert.Equal(["A", "B"], entrees.Select(e => e.Livres[0].Titre));
}
/// <summary>
/// Un seul tome possédé forme quand même un bloc : c'est ce qui dit à quoi ce livre
/// appartient, et ce qui mène à la série où figurent les tomes manquants.
/// </summary>
[Fact]
public void Un_seul_tome_possede_forme_quand_meme_un_bloc()
{
var entrees = GroupementCatalogue.Grouper(
[Livre(4, "Terre natale")], [Serie(9, "L'Elfe noir", (0, 4), (1, 5))]);
Assert.Equal("L'Elfe noir", Assert.Single(entrees).Serie!.Titre);
}
/// <summary>
/// Les tomes filtrés restent absents : le regroupement rassemble ce que la liste contient,
/// il ne va pas rechercher ce qu'un filtre a écarté.
/// </summary>
[Fact]
public void Un_filtre_ne_ramene_pas_les_tomes_ecartes()
{
var entrees = GroupementCatalogue.Grouper(
[Livre(5, "L'Éclat de cristal")], [Serie(9, "Drizzt", (0, 4), (1, 5))]);
Assert.Equal(["L'Éclat de cristal"], Assert.Single(entrees).Livres.Select(l => l.Titre));
}
/// <summary>
/// ⚠️ Le cas du 2026-08-22 : un cycle est une série de séries, et le catalogue doit le
/// montrer — sans quoi « replier chaque niveau » n'aurait qu'un seul niveau à replier.
/// </summary>
[Fact]
public void Un_cycle_coiffe_les_series_qu_il_contient()
{
List<LivreDto> catalogue = [Livre(1, "Terre natale"), Livre(2, "L'Éclat de cristal")];
List<SerieDto> series =
[
Serie(1, "La Légende de Drizzt"),
SousSerie(3, "La Trilogie des Elfes noirs", 1, 1, (0, 2)),
SousSerie(2, "L'Elfe noir", 1, 0, (0, 1)),
];
var cycle = Assert.Single(GroupementCatalogue.Grouper(catalogue, series));
Assert.Equal("La Légende de Drizzt", cycle.Serie!.Titre);
Assert.Empty(cycle.Livres);
Assert.Equal(2, cycle.NombreLivres);
// Les sous-séries suivent leur ordre de LECTURE dans le cycle, pas l'alphabet.
Assert.Collection(
cycle.SousEntrees,
e =>
{
Assert.Equal("L'Elfe noir", e.Serie!.Titre);
Assert.Equal(["Terre natale"], e.Livres.Select(l => l.Titre));
},
e =>
{
Assert.Equal("La Trilogie des Elfes noirs", e.Serie!.Titre);
Assert.Equal(["L'Éclat de cristal"], e.Livres.Select(l => l.Titre));
});
}
/// <summary>
/// Le bloc du cycle se pose là où tombe son premier tome, comme celui d'une série simple :
/// l'ordre général du catalogue ne bouge pas.
/// </summary>
[Fact]
public void Le_cycle_prend_la_place_du_premier_tome_de_sa_descendance()
{
List<LivreDto> catalogue =
[
Livre(1, "Dracula"),
Livre(2, "Terre natale"),
Livre(3, "Dune"),
Livre(4, "L'Éclat de cristal"),
];
List<SerieDto> series =
[
Serie(1, "Drizzt"),
SousSerie(2, "L'Elfe noir", 1, 0, (0, 2)),
SousSerie(3, "Les Elfes noirs", 1, 1, (0, 4)),
];
Assert.Collection(
GroupementCatalogue.Grouper(catalogue, series),
e => Assert.Equal("Dracula", Assert.Single(e.Livres).Titre),
e => Assert.Equal("Drizzt", e.Serie!.Titre),
e => Assert.Equal("Dune", Assert.Single(e.Livres).Titre));
}
/// <summary>
/// ⚠️ Un niveau qui ne porte rien n'existe pas : ce serait une indentation qui ne se
/// replierait sur rien, et un titre de série dont aucun tome n'est au catalogue.
/// </summary>
[Fact]
public void Les_series_sans_tome_visible_ne_font_pas_de_niveau()
{
List<SerieDto> series =
[
Serie(1, "Cycle"),
SousSerie(2, "Vide", 1, 0, (0, 99)),
SousSerie(3, "Garnie", 1, 1, (0, 5)),
];
var cycle = Assert.Single(GroupementCatalogue.Grouper([Livre(5, "Un tome")], series));
Assert.Equal("Garnie", Assert.Single(cycle.SousEntrees).Serie!.Titre);
}
/// <summary>
/// Une parenté qui boucle en base ne doit pas faire tourner le regroupement sans fin :
/// même garde-fou que la remontée du serveur, bornée par le nombre de séries.
/// </summary>
[Fact]
public void Une_parente_qui_boucle_ne_bloque_pas_le_catalogue()
{
List<SerieDto> series =
[
SousSerie(1, "A", 2, 0, (0, 1)),
SousSerie(2, "B", 1, 0),
];
var entrees = GroupementCatalogue.Grouper([Livre(1, "Un livre")], series);
Assert.Equal([1], entrees.SelectMany(EntreesEtDescendance).SelectMany(e => e.Livres).Select(l => l.Id));
}
private static IEnumerable<EntreeCatalogue> EntreesEtDescendance(EntreeCatalogue entree) =>
[entree, .. entree.SousEntrees.SelectMany(EntreesEtDescendance)];
}