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>
250 lines
8.9 KiB
C#
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)];
|
|
}
|