Ajout de tests unitaires pour améliorer la couverture globale des tests
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
package com.loremind.application.campaigncontext;
|
||||||
|
|
||||||
|
import com.loremind.domain.campaigncontext.Campaign;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.CampaignPdfAdvisor;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unitaire pour CampaignAdaptService.
|
||||||
|
* Mocks des ports (campagne, brief builder, advisor PDF du Brain).
|
||||||
|
* Vérifie la délégation streamée et l'échec quand la campagne est introuvable.
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class CampaignAdaptServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CampaignRepository campaignRepository;
|
||||||
|
@Mock
|
||||||
|
private CampaignBriefBuilder briefBuilder;
|
||||||
|
@Mock
|
||||||
|
private CampaignPdfAdvisor advisor;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CampaignAdaptService service;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAdviseStreaming_DelegatesWithBrief() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").name("Camp").build();
|
||||||
|
when(campaignRepository.findById("camp-1")).thenReturn(Optional.of(campaign));
|
||||||
|
when(briefBuilder.build(campaign)).thenReturn("BRIEF");
|
||||||
|
|
||||||
|
byte[] pdf = {1, 2, 3};
|
||||||
|
Consumer<String> onToken = t -> {};
|
||||||
|
Runnable onComplete = () -> {};
|
||||||
|
Consumer<Throwable> onError = e -> {};
|
||||||
|
|
||||||
|
service.adviseStreaming("camp-1", pdf, "doc.pdf", "[]", onToken, onComplete, onError);
|
||||||
|
|
||||||
|
// Le brief construit doit être relayé tel quel à l'advisor, avec les mêmes callbacks.
|
||||||
|
verify(advisor).adviseStreaming(
|
||||||
|
eq(pdf), eq("doc.pdf"), eq("BRIEF"), eq("[]"),
|
||||||
|
eq(onToken), eq(onComplete), eq(onError));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAdviseStreaming_CampaignNotFound_Throws() {
|
||||||
|
when(campaignRepository.findById("missing")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> service.adviseStreaming("missing", new byte[]{1}, "doc.pdf", "[]",
|
||||||
|
t -> {}, () -> {}, e -> {}));
|
||||||
|
assertEquals("Campagne introuvable : missing", ex.getMessage());
|
||||||
|
|
||||||
|
verify(briefBuilder, never()).build(any());
|
||||||
|
verifyNoInteractions(advisor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package com.loremind.application.campaigncontext;
|
||||||
|
|
||||||
|
import com.loremind.application.generationcontext.CampaignStructuralContextBuilder;
|
||||||
|
import com.loremind.application.generationcontext.LoreStructuralContextBuilder;
|
||||||
|
import com.loremind.domain.campaigncontext.Campaign;
|
||||||
|
import com.loremind.domain.generationcontext.CampaignStructuralContext;
|
||||||
|
import com.loremind.domain.generationcontext.CampaignStructuralContext.ArcSummary;
|
||||||
|
import com.loremind.domain.generationcontext.CampaignStructuralContext.ChapterSummary;
|
||||||
|
import com.loremind.domain.generationcontext.CampaignStructuralContext.NpcSummary;
|
||||||
|
import com.loremind.domain.generationcontext.CampaignStructuralContext.SceneSummary;
|
||||||
|
import com.loremind.domain.generationcontext.LoreStructuralContext;
|
||||||
|
import com.loremind.domain.generationcontext.LoreStructuralContext.PageSummary;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unitaire pour CampaignBriefBuilder.
|
||||||
|
* Mocks des deux builders de contexte structurel (campagne + lore).
|
||||||
|
* Vérifie l'assemblage du markdown : titres, vocabulaire HUB/LINEAR, PNJ, lore conditionnel.
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class CampaignBriefBuilderTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CampaignStructuralContextBuilder campaignContextBuilder;
|
||||||
|
@Mock
|
||||||
|
private LoreStructuralContextBuilder loreContextBuilder;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CampaignBriefBuilder builder;
|
||||||
|
|
||||||
|
private static CampaignStructuralContext emptyCtx(String name, String description) {
|
||||||
|
return new CampaignStructuralContext(name, description, List.of(), List.of(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuild_EmptyCampaign_HeaderAndNoArcsPlaceholder() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").build();
|
||||||
|
when(campaignContextBuilder.build("camp-1")).thenReturn(emptyCtx("Ma Campagne", "Un synopsis"));
|
||||||
|
|
||||||
|
String result = builder.build(campaign);
|
||||||
|
|
||||||
|
assertTrue(result.contains("# Campagne : Ma Campagne"));
|
||||||
|
assertTrue(result.contains("Un synopsis"));
|
||||||
|
assertTrue(result.contains("## Structure (arcs → chapitres → scènes)"));
|
||||||
|
assertTrue(result.contains("_(aucun arc pour le moment)_"));
|
||||||
|
// Pas de PNJ ni de lore.
|
||||||
|
assertFalse(result.contains("## PNJ existants"));
|
||||||
|
assertFalse(result.contains("## Univers"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuild_BlankDescriptionOmitted() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").build();
|
||||||
|
when(campaignContextBuilder.build("camp-1")).thenReturn(emptyCtx("Camp", " "));
|
||||||
|
|
||||||
|
String result = builder.build(campaign);
|
||||||
|
|
||||||
|
assertTrue(result.contains("# Campagne : Camp"));
|
||||||
|
// La description blanche ne doit pas générer de ligne propre.
|
||||||
|
assertFalse(result.contains(" \n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuild_HubArcUsesQuestVocabulary() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").build();
|
||||||
|
SceneSummary scene = new SceneSummary("Scène A", "desc scène", 0, List.of(), List.of());
|
||||||
|
ChapterSummary chapter = new ChapterSummary("Quête 1", "desc quête", 0, List.of(scene));
|
||||||
|
ArcSummary hubArc = new ArcSummary("Arc Hub", "desc arc", true, 0, List.of(chapter));
|
||||||
|
CampaignStructuralContext ctx = new CampaignStructuralContext(
|
||||||
|
"Camp", null, List.of(hubArc), List.of(), List.of());
|
||||||
|
when(campaignContextBuilder.build("camp-1")).thenReturn(ctx);
|
||||||
|
|
||||||
|
String result = builder.build(campaign);
|
||||||
|
|
||||||
|
assertTrue(result.contains("### Arc HUB (à quêtes) : Arc Hub — desc arc"));
|
||||||
|
assertTrue(result.contains("- Quête : Quête 1 — desc quête"));
|
||||||
|
assertTrue(result.contains(" - Scène : Scène A — desc scène"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuild_LinearArcUsesChapterVocabulary() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").build();
|
||||||
|
ChapterSummary chapter = new ChapterSummary("Chapitre 1", null, 0, List.of());
|
||||||
|
ArcSummary linearArc = new ArcSummary("Arc Linéaire", null, false, 0, List.of(chapter));
|
||||||
|
CampaignStructuralContext ctx = new CampaignStructuralContext(
|
||||||
|
"Camp", null, List.of(linearArc), List.of(), List.of());
|
||||||
|
when(campaignContextBuilder.build("camp-1")).thenReturn(ctx);
|
||||||
|
|
||||||
|
String result = builder.build(campaign);
|
||||||
|
|
||||||
|
assertTrue(result.contains("### Arc : Arc Linéaire"));
|
||||||
|
assertTrue(result.contains("- Chapitre : Chapitre 1"));
|
||||||
|
// La légende mentionne toujours « HUB » ; ce qui compte est que l'arc LINEAR
|
||||||
|
// n'emploie PAS le vocabulaire HUB (en-tête « ### Arc HUB » ni « Quête »).
|
||||||
|
assertFalse(result.contains("### Arc HUB"));
|
||||||
|
assertFalse(result.contains("- Quête :"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuild_IncludesNpcsWhenPresent() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").build();
|
||||||
|
CampaignStructuralContext ctx = new CampaignStructuralContext(
|
||||||
|
"Camp", null, List.of(),
|
||||||
|
List.of(),
|
||||||
|
List.of(new NpcSummary("Gandalf", "magicien gris"),
|
||||||
|
new NpcSummary("Sauron", null)));
|
||||||
|
when(campaignContextBuilder.build("camp-1")).thenReturn(ctx);
|
||||||
|
|
||||||
|
String result = builder.build(campaign);
|
||||||
|
|
||||||
|
assertTrue(result.contains("## PNJ existants"));
|
||||||
|
assertTrue(result.contains("- Gandalf : magicien gris"));
|
||||||
|
assertTrue(result.contains("- Sauron"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuild_AppendsLoreWhenLinked() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").loreId("lore-1").build();
|
||||||
|
when(campaignContextBuilder.build("camp-1")).thenReturn(emptyCtx("Camp", null));
|
||||||
|
|
||||||
|
Map<String, List<PageSummary>> folders = new LinkedHashMap<>();
|
||||||
|
folders.put("Lieux", List.of(
|
||||||
|
new PageSummary("La Comté", "tpl", Map.of(), List.of(), List.of())));
|
||||||
|
LoreStructuralContext lore = new LoreStructuralContext(
|
||||||
|
"Terre du Milieu", "Un vaste monde", folders, List.of());
|
||||||
|
when(loreContextBuilder.buildOptional("lore-1")).thenReturn(Optional.of(lore));
|
||||||
|
|
||||||
|
String result = builder.build(campaign);
|
||||||
|
|
||||||
|
assertTrue(result.contains("## Univers (Lore) : Terre du Milieu"));
|
||||||
|
assertTrue(result.contains("Un vaste monde"));
|
||||||
|
assertTrue(result.contains("### Lieux"));
|
||||||
|
assertTrue(result.contains("- La Comté"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuild_NotLinkedToLore_SkipsLoreBuilder() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").build(); // loreId null
|
||||||
|
when(campaignContextBuilder.build("camp-1")).thenReturn(emptyCtx("Camp", null));
|
||||||
|
|
||||||
|
String result = builder.build(campaign);
|
||||||
|
|
||||||
|
assertFalse(result.contains("## Univers"));
|
||||||
|
verify(loreContextBuilder, never()).buildOptional(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuild_LinkedButLoreAbsent_NoLoreSection() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").loreId("lore-1").build();
|
||||||
|
when(campaignContextBuilder.build("camp-1")).thenReturn(emptyCtx("Camp", null));
|
||||||
|
when(loreContextBuilder.buildOptional("lore-1")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
String result = builder.build(campaign);
|
||||||
|
|
||||||
|
assertFalse(result.contains("## Univers"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.loremind.application.campaigncontext;
|
||||||
|
|
||||||
|
import com.loremind.domain.campaigncontext.Arc;
|
||||||
|
import com.loremind.domain.campaigncontext.Chapter;
|
||||||
|
import com.loremind.domain.campaigncontext.Prerequisite;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.ArcRepository;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.ChapterRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unitaire pour CampaignReferencedFlagsService.
|
||||||
|
* Mocks des ports ArcRepository / ChapterRepository.
|
||||||
|
* Vérifie l'énumération dédupliquée + triée des FlagSet référencés par les chapitres.
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class CampaignReferencedFlagsServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ArcRepository arcRepository;
|
||||||
|
@Mock
|
||||||
|
private ChapterRepository chapterRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CampaignReferencedFlagsService service;
|
||||||
|
|
||||||
|
private static Chapter chapterWithPrereqs(String id, String arcId, Prerequisite... prereqs) {
|
||||||
|
return Chapter.builder()
|
||||||
|
.id(id)
|
||||||
|
.arcId(arcId)
|
||||||
|
.name("C-" + id)
|
||||||
|
.prerequisites(List.of(prereqs))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testListForCampaign_DeduplicatedAndSorted() {
|
||||||
|
// Arrange : deux arcs, plusieurs chapitres, flags dont certains en doublon.
|
||||||
|
Arc arc1 = Arc.builder().id("arc-1").campaignId("camp-1").build();
|
||||||
|
Arc arc2 = Arc.builder().id("arc-2").campaignId("camp-1").build();
|
||||||
|
when(arcRepository.findByCampaignId("camp-1")).thenReturn(List.of(arc1, arc2));
|
||||||
|
|
||||||
|
Chapter c1 = chapterWithPrereqs("c1", "arc-1",
|
||||||
|
new Prerequisite.FlagSet("zeta"),
|
||||||
|
new Prerequisite.FlagSet("alpha"));
|
||||||
|
Chapter c2 = chapterWithPrereqs("c2", "arc-2",
|
||||||
|
new Prerequisite.FlagSet("alpha"), // doublon -> dédupliqué
|
||||||
|
new Prerequisite.FlagSet("beta"));
|
||||||
|
when(chapterRepository.findByArcId("arc-1")).thenReturn(List.of(c1));
|
||||||
|
when(chapterRepository.findByArcId("arc-2")).thenReturn(List.of(c2));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
List<String> result = service.listForCampaign("camp-1");
|
||||||
|
|
||||||
|
// Assert : tri alphabétique + déduplication.
|
||||||
|
assertEquals(List.of("alpha", "beta", "zeta"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testListForCampaign_IgnoresNonFlagSetAndBlankNames() {
|
||||||
|
Arc arc = Arc.builder().id("arc-1").campaignId("camp-1").build();
|
||||||
|
when(arcRepository.findByCampaignId("camp-1")).thenReturn(List.of(arc));
|
||||||
|
|
||||||
|
Chapter chapter = chapterWithPrereqs("c1", "arc-1",
|
||||||
|
new Prerequisite.QuestCompleted("quest-x"), // pas un FlagSet -> ignoré
|
||||||
|
new Prerequisite.SessionReached(3), // pas un FlagSet -> ignoré
|
||||||
|
new Prerequisite.FlagSet(""), // blanc -> ignoré
|
||||||
|
new Prerequisite.FlagSet(" "), // blanc -> ignoré
|
||||||
|
new Prerequisite.FlagSet("real"));
|
||||||
|
when(chapterRepository.findByArcId("arc-1")).thenReturn(List.of(chapter));
|
||||||
|
|
||||||
|
List<String> result = service.listForCampaign("camp-1");
|
||||||
|
|
||||||
|
assertEquals(List.of("real"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testListForCampaign_NullPrerequisitesSkipped() {
|
||||||
|
Arc arc = Arc.builder().id("arc-1").campaignId("camp-1").build();
|
||||||
|
when(arcRepository.findByCampaignId("camp-1")).thenReturn(List.of(arc));
|
||||||
|
|
||||||
|
Chapter chapter = Chapter.builder().id("c1").arcId("arc-1").name("C").prerequisites(null).build();
|
||||||
|
when(chapterRepository.findByArcId("arc-1")).thenReturn(List.of(chapter));
|
||||||
|
|
||||||
|
List<String> result = service.listForCampaign("camp-1");
|
||||||
|
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testListForCampaign_NoArcs() {
|
||||||
|
when(arcRepository.findByCampaignId("camp-1")).thenReturn(List.of());
|
||||||
|
|
||||||
|
List<String> result = service.listForCampaign("camp-1");
|
||||||
|
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
verify(chapterRepository, never()).findByArcId(anyString());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package com.loremind.application.campaigncontext;
|
||||||
|
|
||||||
|
import com.loremind.domain.campaigncontext.Chapter;
|
||||||
|
import com.loremind.domain.campaigncontext.Prerequisite;
|
||||||
|
import com.loremind.domain.campaigncontext.ProgressionStatus;
|
||||||
|
import com.loremind.domain.campaigncontext.QuestStatus;
|
||||||
|
import com.loremind.domain.playcontext.Playthrough;
|
||||||
|
import com.loremind.domain.playcontext.QuestProgression;
|
||||||
|
import com.loremind.domain.playcontext.Session;
|
||||||
|
import com.loremind.domain.playcontext.ports.PlaythroughFlagRepository;
|
||||||
|
import com.loremind.domain.playcontext.ports.PlaythroughRepository;
|
||||||
|
import com.loremind.domain.playcontext.ports.QuestProgressionRepository;
|
||||||
|
import com.loremind.domain.playcontext.ports.SessionRepository;
|
||||||
|
import com.loremind.infrastructure.web.dto.campaigncontext.ChapterDTO;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unitaire pour ChapterStatusEnricher.
|
||||||
|
* Mocks des ports Play Context (playthrough, progression, flags, sessions).
|
||||||
|
* Vérifie la construction du snapshot d'évaluation + le calcul du statut effectif.
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class ChapterStatusEnricherTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private PlaythroughRepository playthroughRepository;
|
||||||
|
@Mock
|
||||||
|
private QuestProgressionRepository progressionRepository;
|
||||||
|
@Mock
|
||||||
|
private PlaythroughFlagRepository flagRepository;
|
||||||
|
@Mock
|
||||||
|
private SessionRepository sessionRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ChapterStatusEnricher enricher;
|
||||||
|
|
||||||
|
// --- buildSnapshot ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildSnapshot_NullPlaythroughId_ReturnsEmptyContext() {
|
||||||
|
// Aucune interaction avec les repos de données : court-circuit.
|
||||||
|
ChapterStatusEnricher.PlaythroughEvalSnapshot snap = enricher.buildSnapshot(null);
|
||||||
|
|
||||||
|
assertNotNull(snap);
|
||||||
|
assertTrue(snap.progressionByChapterId().isEmpty());
|
||||||
|
assertTrue(snap.ctx().completedQuestIds().isEmpty());
|
||||||
|
assertEquals(0, snap.ctx().currentSessionCount());
|
||||||
|
assertTrue(snap.ctx().campaignFlags().isEmpty());
|
||||||
|
verify(flagRepository, never()).findByPlaythroughId(anyString());
|
||||||
|
verify(sessionRepository, never()).findByPlaythroughId(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildSnapshot_UnknownPlaythrough_ReturnsEmptyContext() {
|
||||||
|
when(playthroughRepository.findById("pt-x")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
ChapterStatusEnricher.PlaythroughEvalSnapshot snap = enricher.buildSnapshot("pt-x");
|
||||||
|
|
||||||
|
assertTrue(snap.progressionByChapterId().isEmpty());
|
||||||
|
assertTrue(snap.ctx().completedQuestIds().isEmpty());
|
||||||
|
verify(flagRepository, never()).findByPlaythroughId(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildSnapshot_PopulatesContextAndProgressionMap() {
|
||||||
|
when(playthroughRepository.findById("pt-1"))
|
||||||
|
.thenReturn(Optional.of(Playthrough.builder().id("pt-1").build()));
|
||||||
|
when(flagRepository.findByPlaythroughId("pt-1")).thenReturn(Map.of("door_open", true));
|
||||||
|
when(progressionRepository.findCompletedChapterIdsByPlaythroughId("pt-1"))
|
||||||
|
.thenReturn(Set.of("chap-done"));
|
||||||
|
when(sessionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of(
|
||||||
|
Session.builder().id("s1").build(),
|
||||||
|
Session.builder().id("s2").build()));
|
||||||
|
when(progressionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of(
|
||||||
|
QuestProgression.builder().chapterId("chap-1").status(ProgressionStatus.IN_PROGRESS).build(),
|
||||||
|
QuestProgression.builder().chapterId("chap-2").status(ProgressionStatus.COMPLETED).build()));
|
||||||
|
|
||||||
|
ChapterStatusEnricher.PlaythroughEvalSnapshot snap = enricher.buildSnapshot("pt-1");
|
||||||
|
|
||||||
|
assertEquals(Set.of("chap-done"), snap.ctx().completedQuestIds());
|
||||||
|
assertEquals(2, snap.ctx().currentSessionCount());
|
||||||
|
assertEquals(Map.of("door_open", true), snap.ctx().campaignFlags());
|
||||||
|
assertEquals(ProgressionStatus.IN_PROGRESS, snap.progressionByChapterId().get("chap-1"));
|
||||||
|
assertEquals(ProgressionStatus.COMPLETED, snap.progressionByChapterId().get("chap-2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- computeFor ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testComputeFor_NotStartedWithNoPrereqs_Available() {
|
||||||
|
when(playthroughRepository.findById("pt-1"))
|
||||||
|
.thenReturn(Optional.of(Playthrough.builder().id("pt-1").build()));
|
||||||
|
when(flagRepository.findByPlaythroughId("pt-1")).thenReturn(Map.of());
|
||||||
|
when(progressionRepository.findCompletedChapterIdsByPlaythroughId("pt-1")).thenReturn(Set.of());
|
||||||
|
when(sessionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of());
|
||||||
|
when(progressionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of());
|
||||||
|
|
||||||
|
Chapter chapter = Chapter.builder().id("chap-1").name("C").prerequisites(List.of()).build();
|
||||||
|
|
||||||
|
QuestStatus status = enricher.computeFor(chapter, "pt-1");
|
||||||
|
|
||||||
|
assertEquals(QuestStatus.AVAILABLE, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testComputeFor_NotStartedWithUnmetPrereq_Locked() {
|
||||||
|
when(playthroughRepository.findById("pt-1"))
|
||||||
|
.thenReturn(Optional.of(Playthrough.builder().id("pt-1").build()));
|
||||||
|
when(flagRepository.findByPlaythroughId("pt-1")).thenReturn(Map.of());
|
||||||
|
when(progressionRepository.findCompletedChapterIdsByPlaythroughId("pt-1")).thenReturn(Set.of());
|
||||||
|
when(sessionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of());
|
||||||
|
when(progressionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of());
|
||||||
|
|
||||||
|
Chapter chapter = Chapter.builder().id("chap-1").name("C")
|
||||||
|
.prerequisites(List.of(new Prerequisite.QuestCompleted("other"))).build();
|
||||||
|
|
||||||
|
QuestStatus status = enricher.computeFor(chapter, "pt-1");
|
||||||
|
|
||||||
|
assertEquals(QuestStatus.LOCKED, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testComputeFor_CompletedProgression_Completed() {
|
||||||
|
when(playthroughRepository.findById("pt-1"))
|
||||||
|
.thenReturn(Optional.of(Playthrough.builder().id("pt-1").build()));
|
||||||
|
when(flagRepository.findByPlaythroughId("pt-1")).thenReturn(Map.of());
|
||||||
|
when(progressionRepository.findCompletedChapterIdsByPlaythroughId("pt-1")).thenReturn(Set.of());
|
||||||
|
when(sessionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of());
|
||||||
|
when(progressionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of(
|
||||||
|
QuestProgression.builder().chapterId("chap-1").status(ProgressionStatus.COMPLETED).build()));
|
||||||
|
|
||||||
|
Chapter chapter = Chapter.builder().id("chap-1").name("C").prerequisites(List.of()).build();
|
||||||
|
|
||||||
|
QuestStatus status = enricher.computeFor(chapter, "pt-1");
|
||||||
|
|
||||||
|
assertEquals(QuestStatus.COMPLETED, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testComputeFor_NoPlaythrough_FallsBackToAvailableWhenNoPrereqs() {
|
||||||
|
// playthroughId null -> snapshot vide -> NOT_STARTED par défaut.
|
||||||
|
Chapter chapter = Chapter.builder().id("chap-1").name("C").prerequisites(List.of()).build();
|
||||||
|
|
||||||
|
QuestStatus status = enricher.computeFor(chapter, null);
|
||||||
|
|
||||||
|
assertEquals(QuestStatus.AVAILABLE, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- enrich ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEnrich_NullOrEmptyDtos_NoSnapshotBuilt() {
|
||||||
|
enricher.enrich(null, List.of(), "pt-1");
|
||||||
|
enricher.enrich(List.of(), List.of(), "pt-1");
|
||||||
|
|
||||||
|
// Aucun snapshot construit -> pas d'accès au repo playthrough.
|
||||||
|
verify(playthroughRepository, never()).findById(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEnrich_InjectsStatusesIntoMatchingDtos() {
|
||||||
|
when(playthroughRepository.findById("pt-1"))
|
||||||
|
.thenReturn(Optional.of(Playthrough.builder().id("pt-1").build()));
|
||||||
|
when(flagRepository.findByPlaythroughId("pt-1")).thenReturn(Map.of());
|
||||||
|
when(progressionRepository.findCompletedChapterIdsByPlaythroughId("pt-1")).thenReturn(Set.of());
|
||||||
|
when(sessionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of());
|
||||||
|
when(progressionRepository.findByPlaythroughId("pt-1")).thenReturn(List.of(
|
||||||
|
QuestProgression.builder().chapterId("chap-1").status(ProgressionStatus.IN_PROGRESS).build()));
|
||||||
|
|
||||||
|
Chapter c1 = Chapter.builder().id("chap-1").name("C1").prerequisites(List.of()).build();
|
||||||
|
Chapter c2 = Chapter.builder().id("chap-2").name("C2").prerequisites(List.of()).build();
|
||||||
|
|
||||||
|
ChapterDTO d1 = new ChapterDTO();
|
||||||
|
d1.setId("chap-1");
|
||||||
|
ChapterDTO d2 = new ChapterDTO();
|
||||||
|
d2.setId("chap-2");
|
||||||
|
ChapterDTO orphan = new ChapterDTO();
|
||||||
|
orphan.setId("chap-unknown"); // pas dans domain -> ignoré
|
||||||
|
|
||||||
|
enricher.enrich(List.of(d1, d2, orphan), List.of(c1, c2), "pt-1");
|
||||||
|
|
||||||
|
// chap-1 : progression IN_PROGRESS -> effectif IN_PROGRESS.
|
||||||
|
assertEquals("IN_PROGRESS", d1.getProgressionStatus());
|
||||||
|
assertEquals("IN_PROGRESS", d1.getEffectiveStatus());
|
||||||
|
// chap-2 : pas de progression -> NOT_STARTED + sans prereq -> AVAILABLE.
|
||||||
|
assertEquals("NOT_STARTED", d2.getProgressionStatus());
|
||||||
|
assertEquals("AVAILABLE", d2.getEffectiveStatus());
|
||||||
|
// orphelin : non touché.
|
||||||
|
assertNull(orphan.getProgressionStatus());
|
||||||
|
assertNull(orphan.getEffectiveStatus());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package com.loremind.application.campaigncontext;
|
||||||
|
|
||||||
|
import com.loremind.domain.campaigncontext.Character;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.CharacterRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unitaire pour CharacterService.
|
||||||
|
* Mock du port CharacterRepository.
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class CharacterServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CharacterRepository characterRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CharacterService service;
|
||||||
|
|
||||||
|
private static CharacterService.CharacterData data(Integer order, String playthroughId) {
|
||||||
|
return new CharacterService.CharacterData(
|
||||||
|
"Aragorn", "portrait-1", "header-1",
|
||||||
|
Map.of("Classe", "Rôdeur"),
|
||||||
|
Map.of("Galerie", List.of("img-1")),
|
||||||
|
Map.of("Stats", Map.of("FOR", "16")),
|
||||||
|
playthroughId, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- createCharacter ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateCharacter_WithExplicitOrder() {
|
||||||
|
when(characterRepository.save(any(Character.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Character result = service.createCharacter(data(4, "pt-1"));
|
||||||
|
|
||||||
|
assertEquals("Aragorn", result.getName());
|
||||||
|
assertEquals(4, result.getOrder());
|
||||||
|
assertEquals("Rôdeur", result.getValues().get("Classe"));
|
||||||
|
assertEquals("pt-1", result.getPlaythroughId());
|
||||||
|
verify(characterRepository, never()).findByPlaythroughId(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateCharacter_ComputesNextOrderWhenNull() {
|
||||||
|
when(characterRepository.findByPlaythroughId("pt-1")).thenReturn(List.of(
|
||||||
|
Character.builder().order(1).build(),
|
||||||
|
Character.builder().order(4).build()));
|
||||||
|
when(characterRepository.save(any(Character.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Character result = service.createCharacter(data(null, "pt-1"));
|
||||||
|
|
||||||
|
assertEquals(5, result.getOrder()); // max(1,4)+1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateCharacter_NextOrderZeroWhenNoExisting() {
|
||||||
|
when(characterRepository.findByPlaythroughId("pt-1")).thenReturn(List.of());
|
||||||
|
when(characterRepository.save(any(Character.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Character result = service.createCharacter(data(null, "pt-1"));
|
||||||
|
|
||||||
|
assertEquals(0, result.getOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateCharacter_NullMapsBecomeEmpty() {
|
||||||
|
when(characterRepository.save(any(Character.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
CharacterService.CharacterData d = new CharacterService.CharacterData(
|
||||||
|
"Bob", null, null, null, null, null, "pt-1", 0);
|
||||||
|
|
||||||
|
Character result = service.createCharacter(d);
|
||||||
|
|
||||||
|
assertNotNull(result.getValues());
|
||||||
|
assertTrue(result.getValues().isEmpty());
|
||||||
|
assertNotNull(result.getImageValues());
|
||||||
|
assertTrue(result.getImageValues().isEmpty());
|
||||||
|
assertNotNull(result.getKeyValueValues());
|
||||||
|
assertTrue(result.getKeyValueValues().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- read ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetCharacterById_Found() {
|
||||||
|
Character c = Character.builder().id("c-1").name("Aragorn").build();
|
||||||
|
when(characterRepository.findById("c-1")).thenReturn(Optional.of(c));
|
||||||
|
|
||||||
|
Optional<Character> result = service.getCharacterById("c-1");
|
||||||
|
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertEquals("Aragorn", result.get().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetCharactersByPlaythroughId() {
|
||||||
|
when(characterRepository.findByPlaythroughId("pt-1"))
|
||||||
|
.thenReturn(List.of(Character.builder().id("c-1").build()));
|
||||||
|
|
||||||
|
List<Character> result = service.getCharactersByPlaythroughId("pt-1");
|
||||||
|
|
||||||
|
assertEquals(1, result.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- updateCharacter ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateCharacter_Success() {
|
||||||
|
Character existing = Character.builder().id("c-1").name("Old").order(2).playthroughId("pt-1").build();
|
||||||
|
when(characterRepository.findById("c-1")).thenReturn(Optional.of(existing));
|
||||||
|
when(characterRepository.save(any(Character.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Character result = service.updateCharacter("c-1", data(null, "pt-2"));
|
||||||
|
|
||||||
|
assertEquals("Aragorn", result.getName());
|
||||||
|
assertEquals("Rôdeur", result.getValues().get("Classe"));
|
||||||
|
// order null -> conserve l'ordre existant.
|
||||||
|
assertEquals(2, result.getOrder());
|
||||||
|
// playthroughId immuable : inchangé malgré data.playthroughId="pt-2".
|
||||||
|
assertEquals("pt-1", result.getPlaythroughId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateCharacter_AppliesOrderWhenProvided() {
|
||||||
|
Character existing = Character.builder().id("c-1").name("Old").order(2).build();
|
||||||
|
when(characterRepository.findById("c-1")).thenReturn(Optional.of(existing));
|
||||||
|
when(characterRepository.save(any(Character.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Character result = service.updateCharacter("c-1", data(8, "pt-1"));
|
||||||
|
|
||||||
|
assertEquals(8, result.getOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateCharacter_NotFound() {
|
||||||
|
when(characterRepository.findById("missing")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> service.updateCharacter("missing", data(1, "pt-1")));
|
||||||
|
assertEquals("Character non trouvé avec l'ID: missing", ex.getMessage());
|
||||||
|
verify(characterRepository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- delete ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteCharacter() {
|
||||||
|
service.deleteCharacter("c-1");
|
||||||
|
verify(characterRepository).deleteById("c-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- searchCharacters ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSearchCharacters_BlankReturnsEmpty() {
|
||||||
|
assertTrue(service.searchCharacters(null).isEmpty());
|
||||||
|
assertTrue(service.searchCharacters(" ").isEmpty());
|
||||||
|
verify(characterRepository, never()).searchByName(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSearchCharacters_TrimsAndDelegates() {
|
||||||
|
when(characterRepository.searchByName("ara")).thenReturn(List.of(Character.builder().id("c-1").build()));
|
||||||
|
|
||||||
|
List<Character> result = service.searchCharacters(" ara ");
|
||||||
|
|
||||||
|
assertEquals(1, result.size());
|
||||||
|
verify(characterRepository).searchByName("ara");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package com.loremind.application.campaigncontext;
|
||||||
|
|
||||||
|
import com.loremind.domain.campaigncontext.Campaign;
|
||||||
|
import com.loremind.domain.campaigncontext.CatalogItem;
|
||||||
|
import com.loremind.domain.campaigncontext.ItemCatalog;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.ItemCatalogGenerator;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.ItemCatalogRepository;
|
||||||
|
import com.loremind.domain.gamesystemcontext.GameSystem;
|
||||||
|
import com.loremind.domain.gamesystemcontext.ports.GameSystemRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unitaire pour ItemCatalogService.
|
||||||
|
* Mocks des ports (repository, générateur IA, campagne, système de jeu).
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class ItemCatalogServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private ItemCatalogRepository repository;
|
||||||
|
@Mock
|
||||||
|
private ItemCatalogGenerator generator;
|
||||||
|
@Mock
|
||||||
|
private CampaignRepository campaignRepository;
|
||||||
|
@Mock
|
||||||
|
private GameSystemRepository gameSystemRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private ItemCatalogService service;
|
||||||
|
|
||||||
|
private static ItemCatalogService.CatalogData data(Integer order, String campaignId) {
|
||||||
|
return new ItemCatalogService.CatalogData(
|
||||||
|
"Échoppe", "desc", "icon",
|
||||||
|
List.of(CatalogItem.builder().name("Épée").price("50 po").build()),
|
||||||
|
campaignId, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- createCatalog ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateCatalog_WithExplicitOrder() {
|
||||||
|
when(repository.save(any(ItemCatalog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
ItemCatalog result = service.createCatalog(data(3, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals("Échoppe", result.getName());
|
||||||
|
assertEquals(3, result.getOrder());
|
||||||
|
assertEquals(1, result.getItems().size());
|
||||||
|
verify(repository, never()).findByCampaignId(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateCatalog_ComputesNextOrderWhenNull() {
|
||||||
|
when(repository.findByCampaignId("camp-1")).thenReturn(List.of(
|
||||||
|
ItemCatalog.builder().order(0).build(),
|
||||||
|
ItemCatalog.builder().order(3).build()));
|
||||||
|
when(repository.save(any(ItemCatalog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
ItemCatalog result = service.createCatalog(data(null, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals(4, result.getOrder()); // max(0,3)+1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateCatalog_NextOrderZeroWhenNoExisting() {
|
||||||
|
when(repository.findByCampaignId("camp-1")).thenReturn(List.of());
|
||||||
|
when(repository.save(any(ItemCatalog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
ItemCatalog result = service.createCatalog(data(null, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals(0, result.getOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateCatalog_NullItemsBecomeEmpty() {
|
||||||
|
when(repository.save(any(ItemCatalog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
ItemCatalogService.CatalogData d = new ItemCatalogService.CatalogData(
|
||||||
|
"C", null, null, null, "camp-1", 0);
|
||||||
|
|
||||||
|
ItemCatalog result = service.createCatalog(d);
|
||||||
|
|
||||||
|
assertNotNull(result.getItems());
|
||||||
|
assertTrue(result.getItems().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- read ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetCatalogById_Found() {
|
||||||
|
ItemCatalog c = ItemCatalog.builder().id("c-1").name("C").build();
|
||||||
|
when(repository.findById("c-1")).thenReturn(Optional.of(c));
|
||||||
|
|
||||||
|
Optional<ItemCatalog> result = service.getCatalogById("c-1");
|
||||||
|
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertEquals("C", result.get().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetCatalogsByCampaignId() {
|
||||||
|
when(repository.findByCampaignId("camp-1")).thenReturn(List.of(ItemCatalog.builder().id("c-1").build()));
|
||||||
|
|
||||||
|
List<ItemCatalog> result = service.getCatalogsByCampaignId("camp-1");
|
||||||
|
|
||||||
|
assertEquals(1, result.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- updateCatalog ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateCatalog_Success() {
|
||||||
|
ItemCatalog existing = ItemCatalog.builder().id("c-1").name("Old").order(2).build();
|
||||||
|
when(repository.findById("c-1")).thenReturn(Optional.of(existing));
|
||||||
|
when(repository.save(any(ItemCatalog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
ItemCatalog result = service.updateCatalog("c-1", data(null, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals("Échoppe", result.getName());
|
||||||
|
assertEquals(1, result.getItems().size());
|
||||||
|
// order null -> conserve l'ordre existant.
|
||||||
|
assertEquals(2, result.getOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateCatalog_AppliesOrderWhenProvided() {
|
||||||
|
ItemCatalog existing = ItemCatalog.builder().id("c-1").name("Old").order(2).build();
|
||||||
|
when(repository.findById("c-1")).thenReturn(Optional.of(existing));
|
||||||
|
when(repository.save(any(ItemCatalog.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
ItemCatalog result = service.updateCatalog("c-1", data(6, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals(6, result.getOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateCatalog_NotFound() {
|
||||||
|
when(repository.findById("missing")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> service.updateCatalog("missing", data(1, "camp-1")));
|
||||||
|
assertEquals("Catalogue d'objets introuvable: missing", ex.getMessage());
|
||||||
|
verify(repository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- delete ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteCatalog() {
|
||||||
|
service.deleteCatalog("c-1");
|
||||||
|
verify(repository).deleteById("c-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- searchCatalogs ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSearchCatalogs_BlankReturnsEmpty() {
|
||||||
|
assertTrue(service.searchCatalogs(null).isEmpty());
|
||||||
|
assertTrue(service.searchCatalogs(" ").isEmpty());
|
||||||
|
verify(repository, never()).searchByName(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSearchCatalogs_TrimsAndDelegates() {
|
||||||
|
when(repository.searchByName("epee")).thenReturn(List.of(ItemCatalog.builder().id("c-1").build()));
|
||||||
|
|
||||||
|
List<ItemCatalog> result = service.searchCatalogs(" epee ");
|
||||||
|
|
||||||
|
assertEquals(1, result.size());
|
||||||
|
verify(repository).searchByName("epee");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- generateProposal ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGenerateProposal_CopiesItemsAndContext() {
|
||||||
|
CatalogItem item = CatalogItem.builder().name("Potion").build();
|
||||||
|
when(generator.generate(eq("desc"), anyString()))
|
||||||
|
.thenReturn(new ItemCatalogGenerator.GeneratedCatalog("Nom IA", "Desc IA", List.of(item)));
|
||||||
|
when(campaignRepository.findById("camp-1"))
|
||||||
|
.thenReturn(Optional.of(Campaign.builder().id("camp-1").name("Camp").build()));
|
||||||
|
|
||||||
|
ItemCatalog result = service.generateProposal("camp-1", "desc");
|
||||||
|
|
||||||
|
assertEquals("Nom IA", result.getName());
|
||||||
|
assertEquals("camp-1", result.getCampaignId());
|
||||||
|
assertEquals(1, result.getItems().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGenerateProposal_NullGeneratedItems() {
|
||||||
|
when(generator.generate(anyString(), anyString()))
|
||||||
|
.thenReturn(new ItemCatalogGenerator.GeneratedCatalog("N", "D", null));
|
||||||
|
when(campaignRepository.findById("camp-1")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
ItemCatalog result = service.generateProposal("camp-1", "desc");
|
||||||
|
|
||||||
|
assertNotNull(result.getItems());
|
||||||
|
assertTrue(result.getItems().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGenerateProposal_BuildsContextWithGameSystem() {
|
||||||
|
when(campaignRepository.findById("camp-1")).thenReturn(Optional.of(Campaign.builder()
|
||||||
|
.id("camp-1").name("Camp").description(" Aventure ").gameSystemId("gs-1").build()));
|
||||||
|
when(gameSystemRepository.findById("gs-1"))
|
||||||
|
.thenReturn(Optional.of(GameSystem.builder().id("gs-1").name("Pathfinder").build()));
|
||||||
|
when(generator.generate(anyString(), any()))
|
||||||
|
.thenReturn(new ItemCatalogGenerator.GeneratedCatalog("N", "D", List.of()));
|
||||||
|
|
||||||
|
service.generateProposal("camp-1", "desc");
|
||||||
|
|
||||||
|
ArgumentCaptor<String> ctxCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
verify(generator).generate(eq("desc"), ctxCaptor.capture());
|
||||||
|
String ctx = ctxCaptor.getValue();
|
||||||
|
assertTrue(ctx.contains("Camp"));
|
||||||
|
assertTrue(ctx.contains("Aventure"));
|
||||||
|
assertTrue(ctx.contains("Pathfinder"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,371 @@
|
|||||||
|
package com.loremind.application.campaigncontext;
|
||||||
|
|
||||||
|
import com.loremind.domain.campaigncontext.Campaign;
|
||||||
|
import com.loremind.domain.campaigncontext.Notebook;
|
||||||
|
import com.loremind.domain.campaigncontext.NotebookMessage;
|
||||||
|
import com.loremind.domain.campaigncontext.NotebookSource;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.NotebookIndexer;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.NotebookRepository;
|
||||||
|
import com.loremind.domain.gamesystemcontext.GameSystem;
|
||||||
|
import com.loremind.domain.gamesystemcontext.ports.GameSystemRepository;
|
||||||
|
import com.loremind.domain.shared.template.FieldType;
|
||||||
|
import com.loremind.domain.shared.template.TemplateField;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unitaire pour NotebookService.
|
||||||
|
* Mocks des ports (repository, indexer Brain, campagne, brief builder, système).
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class NotebookServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private NotebookRepository repository;
|
||||||
|
@Mock
|
||||||
|
private NotebookIndexer indexer;
|
||||||
|
@Mock
|
||||||
|
private CampaignRepository campaignRepository;
|
||||||
|
@Mock
|
||||||
|
private CampaignBriefBuilder briefBuilder;
|
||||||
|
@Mock
|
||||||
|
private GameSystemRepository gameSystemRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private NotebookService service;
|
||||||
|
|
||||||
|
// --- createNotebook ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateNotebook_UsesProvidedName() {
|
||||||
|
when(repository.save(any(Notebook.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Notebook result = service.createNotebook("camp-1", " Mon atelier ");
|
||||||
|
|
||||||
|
assertEquals("Mon atelier", result.getName()); // trim
|
||||||
|
assertEquals("camp-1", result.getCampaignId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateNotebook_DefaultNameWhenBlank() {
|
||||||
|
when(repository.save(any(Notebook.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Notebook result = service.createNotebook("camp-1", " ");
|
||||||
|
|
||||||
|
assertEquals("Nouvel atelier", result.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateNotebook_DefaultNameWhenNull() {
|
||||||
|
when(repository.save(any(Notebook.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Notebook result = service.createNotebook("camp-1", null);
|
||||||
|
|
||||||
|
assertEquals("Nouvel atelier", result.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- renameNotebook ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRenameNotebook_Success() {
|
||||||
|
Notebook existing = Notebook.builder().id("nb-1").name("Old").build();
|
||||||
|
when(repository.findById("nb-1")).thenReturn(Optional.of(existing));
|
||||||
|
when(repository.save(any(Notebook.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Notebook result = service.renameNotebook("nb-1", " Nouveau ");
|
||||||
|
|
||||||
|
assertEquals("Nouveau", result.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRenameNotebook_BlankKeepsExistingName() {
|
||||||
|
Notebook existing = Notebook.builder().id("nb-1").name("Old").build();
|
||||||
|
when(repository.findById("nb-1")).thenReturn(Optional.of(existing));
|
||||||
|
when(repository.save(any(Notebook.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Notebook result = service.renameNotebook("nb-1", " ");
|
||||||
|
|
||||||
|
assertEquals("Old", result.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRenameNotebook_NotFound() {
|
||||||
|
when(repository.findById("missing")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> service.renameNotebook("missing", "X"));
|
||||||
|
assertEquals("Notebook introuvable: missing", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- deleteNotebook ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteNotebook_DeletesVectorsThenRow() {
|
||||||
|
when(repository.findSourcesByNotebookId("nb-1")).thenReturn(List.of(
|
||||||
|
NotebookSource.builder().id("src-1").build(),
|
||||||
|
NotebookSource.builder().id("src-2").build()));
|
||||||
|
|
||||||
|
service.deleteNotebook("nb-1");
|
||||||
|
|
||||||
|
verify(indexer).delete("src-1");
|
||||||
|
verify(indexer).delete("src-2");
|
||||||
|
verify(repository).deleteById("nb-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- addSource ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddSource_Success_SetsReadyAndCounters() {
|
||||||
|
when(repository.existsById("nb-1")).thenReturn(true);
|
||||||
|
when(repository.saveSource(any(NotebookSource.class))).thenAnswer(inv -> {
|
||||||
|
NotebookSource s = inv.getArgument(0);
|
||||||
|
if (s.getId() == null) s.setId("src-1");
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
when(indexer.index(eq("src-1"), any(), eq("doc.pdf")))
|
||||||
|
.thenReturn(new NotebookIndexer.IndexResult(42, 10, 2));
|
||||||
|
|
||||||
|
NotebookSource result = service.addSource("nb-1", "doc.pdf", new byte[]{1, 2, 3});
|
||||||
|
|
||||||
|
assertEquals("READY", result.getStatus());
|
||||||
|
assertEquals(42, result.getChunkCount());
|
||||||
|
assertEquals(10, result.getPageCount());
|
||||||
|
// saveSource appelé 2 fois : création INDEXING + mise à jour READY.
|
||||||
|
verify(repository, times(2)).saveSource(any(NotebookSource.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddSource_DefaultFilenameWhenBlank() {
|
||||||
|
when(repository.existsById("nb-1")).thenReturn(true);
|
||||||
|
when(repository.saveSource(any(NotebookSource.class))).thenAnswer(inv -> {
|
||||||
|
NotebookSource s = inv.getArgument(0);
|
||||||
|
if (s.getId() == null) s.setId("src-1");
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
when(indexer.index(anyString(), any(), any()))
|
||||||
|
.thenReturn(new NotebookIndexer.IndexResult(1, 1, 0));
|
||||||
|
|
||||||
|
service.addSource("nb-1", " ", new byte[]{1});
|
||||||
|
|
||||||
|
ArgumentCaptor<NotebookSource> captor = ArgumentCaptor.forClass(NotebookSource.class);
|
||||||
|
verify(repository, atLeastOnce()).saveSource(captor.capture());
|
||||||
|
assertEquals("source.pdf", captor.getAllValues().get(0).getFilename());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddSource_NotebookNotFound() {
|
||||||
|
when(repository.existsById("missing")).thenReturn(false);
|
||||||
|
|
||||||
|
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> service.addSource("missing", "doc.pdf", new byte[]{1}));
|
||||||
|
assertEquals("Notebook introuvable: missing", ex.getMessage());
|
||||||
|
verify(repository, never()).saveSource(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddSource_IndexingFailure_SetsFailedAndRethrows() {
|
||||||
|
when(repository.existsById("nb-1")).thenReturn(true);
|
||||||
|
when(repository.saveSource(any(NotebookSource.class))).thenAnswer(inv -> {
|
||||||
|
NotebookSource s = inv.getArgument(0);
|
||||||
|
if (s.getId() == null) s.setId("src-1");
|
||||||
|
return s;
|
||||||
|
});
|
||||||
|
when(indexer.index(anyString(), any(), any()))
|
||||||
|
.thenThrow(new RuntimeException("Brain KO"));
|
||||||
|
|
||||||
|
RuntimeException ex = assertThrows(RuntimeException.class,
|
||||||
|
() -> service.addSource("nb-1", "doc.pdf", new byte[]{1}));
|
||||||
|
assertEquals("Brain KO", ex.getMessage());
|
||||||
|
|
||||||
|
ArgumentCaptor<NotebookSource> captor = ArgumentCaptor.forClass(NotebookSource.class);
|
||||||
|
verify(repository, times(2)).saveSource(captor.capture());
|
||||||
|
assertEquals("FAILED", captor.getAllValues().get(1).getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- deleteSource ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteSource_DeletesVectorsThenRow() {
|
||||||
|
when(repository.findSourceById("src-1"))
|
||||||
|
.thenReturn(Optional.of(NotebookSource.builder().id("src-1").build()));
|
||||||
|
|
||||||
|
service.deleteSource("src-1");
|
||||||
|
|
||||||
|
verify(indexer).delete("src-1");
|
||||||
|
verify(repository).deleteSourceById("src-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteSource_UnknownSource_StillDeletesRow() {
|
||||||
|
when(repository.findSourceById("src-x")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
service.deleteSource("src-x");
|
||||||
|
|
||||||
|
verify(indexer, never()).delete(anyString());
|
||||||
|
verify(repository).deleteSourceById("src-x");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- readySourceIds ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testReadySourceIds_FiltersOnReadyStatus() {
|
||||||
|
when(repository.findSourcesByNotebookId("nb-1")).thenReturn(List.of(
|
||||||
|
NotebookSource.builder().id("a").status("READY").build(),
|
||||||
|
NotebookSource.builder().id("b").status("INDEXING").build(),
|
||||||
|
NotebookSource.builder().id("c").status("READY").build(),
|
||||||
|
NotebookSource.builder().id("d").status("FAILED").build()));
|
||||||
|
|
||||||
|
List<String> result = service.readySourceIds("nb-1");
|
||||||
|
|
||||||
|
assertEquals(List.of("a", "c"), result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- messages ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testAddMessage_DelegatesToRepo() {
|
||||||
|
when(repository.saveMessage(any(NotebookMessage.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
NotebookMessage result = service.addMessage("nb-1", "user", "salut");
|
||||||
|
|
||||||
|
assertEquals("nb-1", result.getNotebookId());
|
||||||
|
assertEquals("user", result.getRole());
|
||||||
|
assertEquals("salut", result.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testClearChat_Archives() {
|
||||||
|
service.clearChat("nb-1");
|
||||||
|
verify(repository).archiveMessagesByNotebookId("nb-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- buildArchiveContext ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildArchiveContext_EmptyKeysReturnsEmpty() {
|
||||||
|
assertEquals("", service.buildArchiveContext("nb-1", null));
|
||||||
|
assertEquals("", service.buildArchiveContext("nb-1", List.of()));
|
||||||
|
verify(repository, never()).findArchivedMessagesByNotebookId(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildArchiveContext_NoMatchingKeysReturnsEmpty() {
|
||||||
|
LocalDateTime at = LocalDateTime.of(2026, 1, 1, 10, 0);
|
||||||
|
when(repository.findArchivedMessagesByNotebookId("nb-1")).thenReturn(List.of(
|
||||||
|
NotebookMessage.builder().role("user").content("hi").archivedAt(at).build()));
|
||||||
|
|
||||||
|
String result = service.buildArchiveContext("nb-1", List.of("2099-12-31T00:00"));
|
||||||
|
|
||||||
|
assertEquals("", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildArchiveContext_FormatsSelectedArchive() {
|
||||||
|
LocalDateTime at = LocalDateTime.of(2026, 1, 1, 10, 0);
|
||||||
|
when(repository.findArchivedMessagesByNotebookId("nb-1")).thenReturn(List.of(
|
||||||
|
NotebookMessage.builder().role("user").content("Question ?").archivedAt(at).build(),
|
||||||
|
NotebookMessage.builder().role("assistant").content("Réponse.").archivedAt(at).build()));
|
||||||
|
|
||||||
|
String result = service.buildArchiveContext("nb-1", List.of(at.toString()));
|
||||||
|
|
||||||
|
assertTrue(result.contains("ANCIENNES CONVERSATIONS"));
|
||||||
|
assertTrue(result.contains("[Archive du " + at + "]"));
|
||||||
|
assertTrue(result.contains("MJ : Question ?"));
|
||||||
|
assertTrue(result.contains("IA : Réponse."));
|
||||||
|
assertTrue(result.contains("FIN DES ANCIENNES CONVERSATIONS"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildArchiveContext_TruncatesFromStartWhenTooLong() {
|
||||||
|
LocalDateTime at = LocalDateTime.of(2026, 1, 1, 10, 0);
|
||||||
|
// Budget pour 1 seule archive = max(2000, 16000/1) = 16000 chars. Au-delà,
|
||||||
|
// la troncature se fait PAR LE DÉBUT : la fin (conclusion) doit survivre.
|
||||||
|
String head = "A".repeat(17000);
|
||||||
|
String tail = "CONCLUSION_UTILE";
|
||||||
|
when(repository.findArchivedMessagesByNotebookId("nb-1")).thenReturn(List.of(
|
||||||
|
NotebookMessage.builder().role("assistant").content(head + tail).archivedAt(at).build()));
|
||||||
|
|
||||||
|
String result = service.buildArchiveContext("nb-1", List.of(at.toString()));
|
||||||
|
|
||||||
|
assertTrue(result.contains("[…début tronqué…]"));
|
||||||
|
assertTrue(result.contains(tail));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- buildContext (campagne) ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildContext_NullCampaignIdReturnsEmpty() {
|
||||||
|
assertEquals("", service.buildContext(null));
|
||||||
|
verify(briefBuilder, never()).build(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildContext_UnknownCampaignReturnsEmpty() {
|
||||||
|
when(campaignRepository.findById("camp-x")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertEquals("", service.buildContext("camp-x"));
|
||||||
|
verify(briefBuilder, never()).build(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildContext_BriefOnlyWhenNoGameSystem() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").name("Camp").build();
|
||||||
|
when(campaignRepository.findById("camp-1")).thenReturn(Optional.of(campaign));
|
||||||
|
when(briefBuilder.build(campaign)).thenReturn("BRIEF");
|
||||||
|
|
||||||
|
String result = service.buildContext("camp-1");
|
||||||
|
|
||||||
|
assertEquals("BRIEF", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildContext_AppendsNpcTextFields() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").name("Camp").gameSystemId("gs-1").build();
|
||||||
|
when(campaignRepository.findById("camp-1")).thenReturn(Optional.of(campaign));
|
||||||
|
when(briefBuilder.build(campaign)).thenReturn("BRIEF");
|
||||||
|
|
||||||
|
GameSystem gs = GameSystem.builder()
|
||||||
|
.id("gs-1")
|
||||||
|
.npcTemplate(List.of(
|
||||||
|
new TemplateField("Histoire", FieldType.TEXT),
|
||||||
|
new TemplateField("Apparence", FieldType.TEXT),
|
||||||
|
new TemplateField("Portrait", FieldType.IMAGE))) // non-TEXT -> exclu
|
||||||
|
.build();
|
||||||
|
when(gameSystemRepository.findById("gs-1")).thenReturn(Optional.of(gs));
|
||||||
|
|
||||||
|
String result = service.buildContext("camp-1");
|
||||||
|
|
||||||
|
assertTrue(result.startsWith("BRIEF\n\n"));
|
||||||
|
assertTrue(result.contains("FICHE PNJ"));
|
||||||
|
assertTrue(result.contains("Histoire"));
|
||||||
|
assertTrue(result.contains("Apparence"));
|
||||||
|
assertFalse(result.contains("Portrait"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testBuildContext_NoNpcTemplateReturnsBriefOnly() {
|
||||||
|
Campaign campaign = Campaign.builder().id("camp-1").name("Camp").gameSystemId("gs-1").build();
|
||||||
|
when(campaignRepository.findById("camp-1")).thenReturn(Optional.of(campaign));
|
||||||
|
when(briefBuilder.build(campaign)).thenReturn("BRIEF");
|
||||||
|
when(gameSystemRepository.findById("gs-1"))
|
||||||
|
.thenReturn(Optional.of(GameSystem.builder().id("gs-1").npcTemplate(null).build()));
|
||||||
|
|
||||||
|
String result = service.buildContext("camp-1");
|
||||||
|
|
||||||
|
assertEquals("BRIEF", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package com.loremind.application.campaigncontext;
|
||||||
|
|
||||||
|
import com.loremind.domain.campaigncontext.Campaign;
|
||||||
|
import com.loremind.domain.campaigncontext.RandomTable;
|
||||||
|
import com.loremind.domain.campaigncontext.RandomTableEntry;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.CampaignRepository;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.RandomTableGenerator;
|
||||||
|
import com.loremind.domain.campaigncontext.ports.RandomTableRepository;
|
||||||
|
import com.loremind.domain.gamesystemcontext.GameSystem;
|
||||||
|
import com.loremind.domain.gamesystemcontext.ports.GameSystemRepository;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test unitaire pour RandomTableService.
|
||||||
|
* Mocks des ports (repository, générateur IA, campagne, système de jeu).
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
public class RandomTableServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RandomTableRepository repository;
|
||||||
|
@Mock
|
||||||
|
private RandomTableGenerator generator;
|
||||||
|
@Mock
|
||||||
|
private CampaignRepository campaignRepository;
|
||||||
|
@Mock
|
||||||
|
private GameSystemRepository gameSystemRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private RandomTableService service;
|
||||||
|
|
||||||
|
private static RandomTableService.TableData data(Integer order, String campaignId) {
|
||||||
|
return new RandomTableService.TableData(
|
||||||
|
"Rencontres", "desc", "1d20", "dice",
|
||||||
|
List.of(RandomTableEntry.builder().minRoll(1).maxRoll(10).label("Gobelins").build()),
|
||||||
|
campaignId, order);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- createTable ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateTable_WithExplicitOrder() {
|
||||||
|
when(repository.save(any(RandomTable.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
RandomTable result = service.createTable(data(5, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals(5, result.getOrder());
|
||||||
|
assertEquals("Rencontres", result.getName());
|
||||||
|
assertEquals(1, result.getEntries().size());
|
||||||
|
// Pas de calcul d'ordre auto puisque order fourni.
|
||||||
|
verify(repository, never()).findByCampaignId(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateTable_ComputesNextOrderWhenNull() {
|
||||||
|
when(repository.findByCampaignId("camp-1")).thenReturn(List.of(
|
||||||
|
RandomTable.builder().order(2).build(),
|
||||||
|
RandomTable.builder().order(7).build()));
|
||||||
|
when(repository.save(any(RandomTable.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
RandomTable result = service.createTable(data(null, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals(8, result.getOrder()); // max(2,7)+1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateTable_NextOrderZeroWhenNoExisting() {
|
||||||
|
when(repository.findByCampaignId("camp-1")).thenReturn(List.of());
|
||||||
|
when(repository.save(any(RandomTable.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
RandomTable result = service.createTable(data(null, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals(0, result.getOrder()); // -1 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateTable_NullEntriesYieldsEmptyList() {
|
||||||
|
when(repository.save(any(RandomTable.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
RandomTableService.TableData d = new RandomTableService.TableData(
|
||||||
|
"T", null, "1d6", null, null, "camp-1", 0);
|
||||||
|
|
||||||
|
RandomTable result = service.createTable(d);
|
||||||
|
|
||||||
|
assertNotNull(result.getEntries());
|
||||||
|
assertTrue(result.getEntries().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- read ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetTableById_Found() {
|
||||||
|
RandomTable t = RandomTable.builder().id("t-1").name("T").build();
|
||||||
|
when(repository.findById("t-1")).thenReturn(Optional.of(t));
|
||||||
|
|
||||||
|
Optional<RandomTable> result = service.getTableById("t-1");
|
||||||
|
|
||||||
|
assertTrue(result.isPresent());
|
||||||
|
assertEquals("T", result.get().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGetTablesByCampaignId() {
|
||||||
|
when(repository.findByCampaignId("camp-1")).thenReturn(List.of(RandomTable.builder().id("t-1").build()));
|
||||||
|
|
||||||
|
List<RandomTable> result = service.getTablesByCampaignId("camp-1");
|
||||||
|
|
||||||
|
assertEquals(1, result.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- updateTable ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateTable_Success() {
|
||||||
|
RandomTable existing = RandomTable.builder().id("t-1").name("Old").order(3).build();
|
||||||
|
when(repository.findById("t-1")).thenReturn(Optional.of(existing));
|
||||||
|
when(repository.save(any(RandomTable.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
RandomTable result = service.updateTable("t-1", data(null, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals("Rencontres", result.getName());
|
||||||
|
assertEquals("1d20", result.getDiceFormula());
|
||||||
|
// order null dans data -> conserve l'ordre existant.
|
||||||
|
assertEquals(3, result.getOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateTable_AppliesOrderWhenProvided() {
|
||||||
|
RandomTable existing = RandomTable.builder().id("t-1").name("Old").order(3).build();
|
||||||
|
when(repository.findById("t-1")).thenReturn(Optional.of(existing));
|
||||||
|
when(repository.save(any(RandomTable.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
RandomTable result = service.updateTable("t-1", data(9, "camp-1"));
|
||||||
|
|
||||||
|
assertEquals(9, result.getOrder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpdateTable_NotFound() {
|
||||||
|
when(repository.findById("missing")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> service.updateTable("missing", data(1, "camp-1")));
|
||||||
|
assertEquals("Table aléatoire introuvable: missing", ex.getMessage());
|
||||||
|
verify(repository, never()).save(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- delete ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDeleteTable() {
|
||||||
|
service.deleteTable("t-1");
|
||||||
|
verify(repository).deleteById("t-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- reorder ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testReorderTables_AssignsPositions() {
|
||||||
|
RandomTable a = RandomTable.builder().id("a").order(99).build();
|
||||||
|
RandomTable b = RandomTable.builder().id("b").order(99).build();
|
||||||
|
when(repository.findById("a")).thenReturn(Optional.of(a));
|
||||||
|
when(repository.findById("b")).thenReturn(Optional.of(b));
|
||||||
|
when(repository.save(any(RandomTable.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
service.reorderTables(List.of("a", "b"));
|
||||||
|
|
||||||
|
assertEquals(0, a.getOrder());
|
||||||
|
assertEquals(1, b.getOrder());
|
||||||
|
verify(repository).save(a);
|
||||||
|
verify(repository).save(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- searchTables ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSearchTables_BlankReturnsEmpty() {
|
||||||
|
assertTrue(service.searchTables(null).isEmpty());
|
||||||
|
assertTrue(service.searchTables(" ").isEmpty());
|
||||||
|
verify(repository, never()).searchByName(anyString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testSearchTables_TrimsAndDelegates() {
|
||||||
|
when(repository.searchByName("orc")).thenReturn(List.of(RandomTable.builder().id("t-1").build()));
|
||||||
|
|
||||||
|
List<RandomTable> result = service.searchTables(" orc ");
|
||||||
|
|
||||||
|
assertEquals(1, result.size());
|
||||||
|
verify(repository).searchByName("orc");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- generateProposal ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGenerateProposal_DefaultsFormulaAndCopiesEntries() {
|
||||||
|
RandomTableEntry e = RandomTableEntry.builder().minRoll(1).maxRoll(1).label("X").build();
|
||||||
|
when(generator.generate(eq("desc"), eq("1d20"), anyString()))
|
||||||
|
.thenReturn(new RandomTableGenerator.GeneratedTable("Nom IA", "Desc IA", List.of(e)));
|
||||||
|
when(campaignRepository.findById("camp-1"))
|
||||||
|
.thenReturn(Optional.of(Campaign.builder().id("camp-1").name("Ma Campagne").build()));
|
||||||
|
|
||||||
|
RandomTable result = service.generateProposal("camp-1", "desc", " ");
|
||||||
|
|
||||||
|
assertEquals("Nom IA", result.getName());
|
||||||
|
assertEquals("1d20", result.getDiceFormula()); // formule blanche -> défaut 1d20
|
||||||
|
assertEquals("camp-1", result.getCampaignId());
|
||||||
|
assertEquals(1, result.getEntries().size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGenerateProposal_NullGeneratedEntries() {
|
||||||
|
when(generator.generate(anyString(), eq("2d6"), anyString()))
|
||||||
|
.thenReturn(new RandomTableGenerator.GeneratedTable("N", "D", null));
|
||||||
|
when(campaignRepository.findById("camp-1")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
RandomTable result = service.generateProposal("camp-1", "desc", "2d6");
|
||||||
|
|
||||||
|
assertNotNull(result.getEntries());
|
||||||
|
assertTrue(result.getEntries().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGenerateProposal_BuildsContextWithGameSystem() {
|
||||||
|
when(campaignRepository.findById("camp-1")).thenReturn(Optional.of(Campaign.builder()
|
||||||
|
.id("camp-1").name("Camp").description(" Une aventure ").gameSystemId("gs-1").build()));
|
||||||
|
when(gameSystemRepository.findById("gs-1"))
|
||||||
|
.thenReturn(Optional.of(GameSystem.builder().id("gs-1").name("D&D 5e").build()));
|
||||||
|
when(generator.generate(anyString(), anyString(), any()))
|
||||||
|
.thenReturn(new RandomTableGenerator.GeneratedTable("N", "D", List.of()));
|
||||||
|
|
||||||
|
service.generateProposal("camp-1", "desc", "1d8");
|
||||||
|
|
||||||
|
ArgumentCaptor<String> ctxCaptor = ArgumentCaptor.forClass(String.class);
|
||||||
|
verify(generator).generate(eq("desc"), eq("1d8"), ctxCaptor.capture());
|
||||||
|
String ctx = ctxCaptor.getValue();
|
||||||
|
assertTrue(ctx.contains("Camp"));
|
||||||
|
assertTrue(ctx.contains("Une aventure"));
|
||||||
|
assertTrue(ctx.contains("D&D 5e"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- improviseRoll ---
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testImproviseRoll_DelegatesToGenerator() {
|
||||||
|
when(campaignRepository.findById("camp-1")).thenReturn(Optional.empty());
|
||||||
|
when(generator.improvise(eq("Rencontres"), eq("Gobelins"), eq("detail"), anyString()))
|
||||||
|
.thenReturn("Un récit improvisé.");
|
||||||
|
|
||||||
|
String result = service.improviseRoll("camp-1", "Rencontres", "Gobelins", "detail");
|
||||||
|
|
||||||
|
assertEquals("Un récit improvisé.", result);
|
||||||
|
verify(generator).improvise("Rencontres", "Gobelins", "detail", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package com.loremind.infrastructure.transfer;
|
||||||
|
|
||||||
|
import com.loremind.domain.campaigncontext.ArcType;
|
||||||
|
import com.loremind.domain.campaigncontext.Prerequisite;
|
||||||
|
import com.loremind.domain.campaigncontext.SceneBranch;
|
||||||
|
import com.loremind.domain.files.ports.FileStorage;
|
||||||
|
import com.loremind.domain.images.ports.ImageStorage;
|
||||||
|
import com.loremind.infrastructure.persistence.entity.*;
|
||||||
|
import com.loremind.infrastructure.persistence.jpa.*;
|
||||||
|
import com.loremind.infrastructure.transfer.dto.ContentExport;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aller-retour EXPORT → ZIP → IMPORT (mode FUSION) du contenu portable.
|
||||||
|
* <p>
|
||||||
|
* On sème un graphe complet (système de jeu, lore/dossiers/template/page, campagne,
|
||||||
|
* arc, chapitres avec prérequis, scène avec branches/ennemis/battlemap, PNJ, ennemi,
|
||||||
|
* personnage, catalogue d'objets, table aléatoire, image + fichier), on l'exporte,
|
||||||
|
* on le réimporte, puis on vérifie :
|
||||||
|
* <ul>
|
||||||
|
* <li>le zip embarque {@code manifest.json}, {@code data.json} et les binaires
|
||||||
|
* d'images/fichiers RÉFÉRENCÉS ;</li>
|
||||||
|
* <li>l'import DOUBLE chaque type (l'export étant global) ;</li>
|
||||||
|
* <li>les clés d'images (uniques) sont RÉUTILISÉES, pas dupliquées ;</li>
|
||||||
|
* <li>les références sont remappées en 2e passe (l'arc importé pointe la page
|
||||||
|
* importée, pas l'originale) ;</li>
|
||||||
|
* <li>le {@code playthroughId} d'un personnage est remis à null à l'import.</li>
|
||||||
|
* </ul>
|
||||||
|
* Le stockage binaire (MinIO) est mocké : aucun appel réseau, et on contrôle ce que
|
||||||
|
* {@code download} renvoie pour les clés référencées.
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@Transactional
|
||||||
|
class ContentExportImportRoundTripTest {
|
||||||
|
|
||||||
|
@Autowired private ExportService exportService;
|
||||||
|
@Autowired private ImportService importService;
|
||||||
|
|
||||||
|
@Autowired private GameSystemJpaRepository gameSystemRepo;
|
||||||
|
@Autowired private LoreJpaRepository loreRepo;
|
||||||
|
@Autowired private LoreNodeJpaRepository loreNodeRepo;
|
||||||
|
@Autowired private TemplateJpaRepository templateRepo;
|
||||||
|
@Autowired private PageJpaRepository pageRepo;
|
||||||
|
@Autowired private CampaignJpaRepository campaignRepo;
|
||||||
|
@Autowired private ArcJpaRepository arcRepo;
|
||||||
|
@Autowired private ChapterJpaRepository chapterRepo;
|
||||||
|
@Autowired private SceneJpaRepository sceneRepo;
|
||||||
|
@Autowired private CharacterJpaRepository characterRepo;
|
||||||
|
@Autowired private NpcJpaRepository npcRepo;
|
||||||
|
@Autowired private EnemyJpaRepository enemyRepo;
|
||||||
|
@Autowired private ItemCatalogJpaRepository itemCatalogRepo;
|
||||||
|
@Autowired private RandomTableJpaRepository randomTableRepo;
|
||||||
|
@Autowired private ImageJpaRepository imageRepo;
|
||||||
|
@Autowired private StoredFileJpaRepository storedFileRepo;
|
||||||
|
|
||||||
|
@MockitoBean private ImageStorage imageStorage;
|
||||||
|
@MockitoBean private FileStorage fileStorage;
|
||||||
|
|
||||||
|
private static final String IMG_KEY = "images/round-trip-abc.png";
|
||||||
|
private static final String FILE_KEY = "files/round-trip-map.json";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void roundTrip_duplicatesEveryContentTypeAndRemapsReferences() throws IOException {
|
||||||
|
// ----- 1. Graphe de contenu riche -----
|
||||||
|
GameSystemJpaEntity gs = gameSystemRepo.save(GameSystemJpaEntity.builder()
|
||||||
|
.name("RT System").description("d").foundryActorType("npc").isPublic(true).build());
|
||||||
|
|
||||||
|
LoreJpaEntity lore = loreRepo.save(LoreJpaEntity.builder().name("RT Lore").description("d").build());
|
||||||
|
LoreNodeJpaEntity rootNode = loreNodeRepo.save(LoreNodeJpaEntity.builder()
|
||||||
|
.name("RT Root").loreId(lore.getId()).order(0).build());
|
||||||
|
loreNodeRepo.save(LoreNodeJpaEntity.builder()
|
||||||
|
.name("RT Child").loreId(lore.getId()).parentId(rootNode.getId()).order(1).build());
|
||||||
|
TemplateJpaEntity template = templateRepo.save(TemplateJpaEntity.builder()
|
||||||
|
.loreId(lore.getId()).name("RT Template").defaultNodeId(rootNode.getId()).build());
|
||||||
|
PageJpaEntity page = pageRepo.save(PageJpaEntity.builder()
|
||||||
|
.loreId(lore.getId()).nodeId(rootNode.getId()).templateId(template.getId())
|
||||||
|
.title("RT Page").imageValues(Map.of("gallery", List.of(IMG_KEY))).build());
|
||||||
|
// Auto-référence : vérifie le remap des relatedPageIds (2e passe).
|
||||||
|
page.setRelatedPageIds(new ArrayList<>(List.of(String.valueOf(page.getId()))));
|
||||||
|
pageRepo.save(page);
|
||||||
|
|
||||||
|
imageRepo.save(ImageJpaEntity.builder()
|
||||||
|
.filename("abc.png").contentType("image/png").sizeBytes(7).storageKey(IMG_KEY).build());
|
||||||
|
StoredFileJpaEntity storedFile = storedFileRepo.save(StoredFileJpaEntity.builder()
|
||||||
|
.filename("map.json").contentType("application/json").sizeBytes(6).storageKey(FILE_KEY).build());
|
||||||
|
|
||||||
|
CampaignJpaEntity campaign = campaignRepo.save(CampaignJpaEntity.builder()
|
||||||
|
.name("RT Campaign").description("d").arcsCount(1)
|
||||||
|
.loreId(String.valueOf(lore.getId())).gameSystemId(String.valueOf(gs.getId())).build());
|
||||||
|
|
||||||
|
ArcJpaEntity arc = arcRepo.save(ArcJpaEntity.builder()
|
||||||
|
.name("RT Arc").campaignId(campaign.getId()).order(0).type(ArcType.HUB)
|
||||||
|
.relatedPageIds(new ArrayList<>(List.of(String.valueOf(page.getId()))))
|
||||||
|
.illustrationImageIds(new ArrayList<>(List.of(IMG_KEY))).build());
|
||||||
|
|
||||||
|
ChapterJpaEntity chapterA = chapterRepo.save(ChapterJpaEntity.builder()
|
||||||
|
.name("RT Chapter A").arcId(arc.getId()).order(0).build());
|
||||||
|
chapterRepo.save(ChapterJpaEntity.builder()
|
||||||
|
.name("RT Chapter B").arcId(arc.getId()).order(1)
|
||||||
|
.prerequisites(new ArrayList<>(List.of(new Prerequisite.QuestCompleted(String.valueOf(chapterA.getId())))))
|
||||||
|
.relatedPageIds(new ArrayList<>(List.of(String.valueOf(page.getId())))).build());
|
||||||
|
ChapterJpaEntity chapterB = chapterRepo.findAll().stream()
|
||||||
|
.filter(c -> "RT Chapter B".equals(c.getName())).findFirst().orElseThrow();
|
||||||
|
|
||||||
|
EnemyJpaEntity enemy = enemyRepo.save(EnemyJpaEntity.builder()
|
||||||
|
.name("RT Enemy").campaignId(campaign.getId()).level("3").folder("Cave")
|
||||||
|
.foundryRef("Compendium.x").foundryStats(Map.of("hp", "11")).order(0).build());
|
||||||
|
|
||||||
|
sceneRepo.save(SceneJpaEntity.builder()
|
||||||
|
.name("RT Scene").chapterId(chapterB.getId()).order(0)
|
||||||
|
.enemyIds(new ArrayList<>(List.of(String.valueOf(enemy.getId()))))
|
||||||
|
.relatedPageIds(new ArrayList<>(List.of(String.valueOf(page.getId()))))
|
||||||
|
.illustrationImageIds(new ArrayList<>(List.of(IMG_KEY)))
|
||||||
|
.battlemapMediaFileId(String.valueOf(storedFile.getId()))
|
||||||
|
.branches(new ArrayList<>(List.of(new SceneBranch("Si fuite", null, "cond")))).build());
|
||||||
|
|
||||||
|
npcRepo.save(NpcJpaEntity.builder()
|
||||||
|
.name("RT Npc").campaignId(campaign.getId()).portraitImageId(IMG_KEY).folder("Ville").order(0)
|
||||||
|
.relatedPageIds(new ArrayList<>(List.of(String.valueOf(page.getId())))).build());
|
||||||
|
|
||||||
|
characterRepo.save(CharacterJpaEntity.builder()
|
||||||
|
.name("RT Hero").campaignId(campaign.getId()).playthroughId(999L).order(0).build());
|
||||||
|
|
||||||
|
ItemCatalogJpaEntity catalog = ItemCatalogJpaEntity.builder()
|
||||||
|
.name("RT Catalog").campaignId(campaign.getId()).order(0).build();
|
||||||
|
CatalogItemJpaEntity item = CatalogItemJpaEntity.builder()
|
||||||
|
.name("Sword").price("10 gp").category("weapon").position(0).catalog(catalog).build();
|
||||||
|
catalog.setItems(new ArrayList<>(List.of(item)));
|
||||||
|
itemCatalogRepo.save(catalog);
|
||||||
|
|
||||||
|
RandomTableJpaEntity table = RandomTableJpaEntity.builder()
|
||||||
|
.name("RT Table").campaignId(campaign.getId()).diceFormula("1d6").order(0).build();
|
||||||
|
RandomTableEntryJpaEntity entry = RandomTableEntryJpaEntity.builder()
|
||||||
|
.minRoll(1).maxRoll(3).label("Gobelins").position(0).randomTable(table).build();
|
||||||
|
table.setEntries(new ArrayList<>(List.of(entry)));
|
||||||
|
randomTableRepo.save(table);
|
||||||
|
|
||||||
|
// Comptes AVANT export (incluent un éventuel seed) — l'export est GLOBAL.
|
||||||
|
long campaignsBefore = campaignRepo.count();
|
||||||
|
long arcsBefore = arcRepo.count();
|
||||||
|
long chaptersBefore = chapterRepo.count();
|
||||||
|
long scenesBefore = sceneRepo.count();
|
||||||
|
long npcsBefore = npcRepo.count();
|
||||||
|
long enemiesBefore = enemyRepo.count();
|
||||||
|
long pagesBefore = pageRepo.count();
|
||||||
|
long gsBefore = gameSystemRepo.count();
|
||||||
|
long imagesBefore = imageRepo.count();
|
||||||
|
Long campaignId0 = campaign.getId();
|
||||||
|
Long pageId0 = page.getId();
|
||||||
|
|
||||||
|
when(imageStorage.download(IMG_KEY)).thenAnswer(inv -> new ByteArrayInputStream("PNGDATA".getBytes()));
|
||||||
|
when(fileStorage.download(FILE_KEY)).thenAnswer(inv -> new ByteArrayInputStream("{\"x\":1}".getBytes()));
|
||||||
|
|
||||||
|
// ----- 2. Export logique + sérialisation ZIP -----
|
||||||
|
ContentExport export = exportService.buildExport("2026-01-02T03:04:05Z");
|
||||||
|
assertEquals(1, export.manifest().formatVersion());
|
||||||
|
assertEquals("2026-01-02T03:04:05Z", export.manifest().exportedAt());
|
||||||
|
assertNotNull(export.manifest().appVersion());
|
||||||
|
assertFalse(export.campaigns().isEmpty());
|
||||||
|
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
exportService.writeZip(export, baos);
|
||||||
|
Map<String, byte[]> zip = readZip(baos.toByteArray());
|
||||||
|
assertTrue(zip.containsKey("manifest.json"));
|
||||||
|
assertTrue(zip.containsKey("data.json"));
|
||||||
|
// Binaire image référencé (portrait/illustration) embarqué + fichier de battlemap.
|
||||||
|
assertTrue(zip.containsKey("images/" + IMG_KEY), "binaire image référencé attendu dans le zip");
|
||||||
|
assertTrue(zip.containsKey("files/" + FILE_KEY), "binaire fichier référencé attendu dans le zip");
|
||||||
|
|
||||||
|
// ----- 3. Import (mode fusion : nouveaux ids) -----
|
||||||
|
ImportResult result = importService.importZip(new ByteArrayInputStream(baos.toByteArray()));
|
||||||
|
|
||||||
|
// Tout le contenu exporté est ré-inséré → chaque type est doublé.
|
||||||
|
assertEquals(2 * campaignsBefore, campaignRepo.count());
|
||||||
|
assertEquals(2 * arcsBefore, arcRepo.count());
|
||||||
|
assertEquals(2 * chaptersBefore, chapterRepo.count());
|
||||||
|
assertEquals(2 * scenesBefore, sceneRepo.count());
|
||||||
|
assertEquals(2 * npcsBefore, npcRepo.count());
|
||||||
|
assertEquals(2 * enemiesBefore, enemyRepo.count());
|
||||||
|
assertEquals(2 * pagesBefore, pageRepo.count());
|
||||||
|
assertEquals(2 * gsBefore, gameSystemRepo.count());
|
||||||
|
// Clé image UNIQUE déjà présente → réutilisée, pas de doublon.
|
||||||
|
assertEquals(imagesBefore, imageRepo.count());
|
||||||
|
assertTrue(result.imagesReused() >= 1, "l'image référencée doit être réutilisée");
|
||||||
|
assertEquals((int) campaignsBefore, result.created().get("campaigns"));
|
||||||
|
|
||||||
|
// ----- 4. Remapping des références (2e passe) -----
|
||||||
|
// 4a. L'arc importé référence la PAGE importée (remappée), pas l'originale.
|
||||||
|
Long importedCampaignId = onlyOther(campaignRepo.findAll().stream()
|
||||||
|
.filter(c -> "RT Campaign".equals(c.getName())).map(CampaignJpaEntity::getId).toList(), campaignId0);
|
||||||
|
Long importedPageId = onlyOther(pageRepo.findAll().stream()
|
||||||
|
.filter(p -> "RT Page".equals(p.getTitle())).map(PageJpaEntity::getId).toList(), pageId0);
|
||||||
|
ArcJpaEntity importedArc = arcRepo.findAll().stream()
|
||||||
|
.filter(a -> "RT Arc".equals(a.getName()) && importedCampaignId.equals(a.getCampaignId()))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
assertTrue(importedArc.getRelatedPageIds().contains(String.valueOf(importedPageId)),
|
||||||
|
"l'arc importé doit pointer la page importée (remap), pas l'originale");
|
||||||
|
assertFalse(importedArc.getRelatedPageIds().contains(String.valueOf(pageId0)));
|
||||||
|
|
||||||
|
// 4b. Le personnage importé a son playthroughId remis à null (hors périmètre).
|
||||||
|
List<CharacterJpaEntity> heroes = characterRepo.findAll().stream()
|
||||||
|
.filter(c -> "RT Hero".equals(c.getName())).toList();
|
||||||
|
assertEquals(2, heroes.size());
|
||||||
|
assertTrue(heroes.stream().anyMatch(c -> c.getPlaythroughId() == null));
|
||||||
|
assertTrue(heroes.stream().anyMatch(c -> Long.valueOf(999L).equals(c.getPlaythroughId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lit toutes les entrées (nom → octets) d'un zip en mémoire. */
|
||||||
|
private static Map<String, byte[]> readZip(byte[] bytes) throws IOException {
|
||||||
|
Map<String, byte[]> out = new LinkedHashMap<>();
|
||||||
|
try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(bytes))) {
|
||||||
|
ZipEntry e;
|
||||||
|
while ((e = zip.getNextEntry()) != null) {
|
||||||
|
if (!e.isDirectory()) {
|
||||||
|
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
||||||
|
zip.transferTo(buf);
|
||||||
|
out.put(e.getName(), buf.toByteArray());
|
||||||
|
}
|
||||||
|
zip.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renvoie l'unique id de la liste différent de {@code original} (l'élément importé). */
|
||||||
|
private static Long onlyOther(List<Long> ids, Long original) {
|
||||||
|
return ids.stream().filter(id -> !id.equals(original)).findFirst().orElseThrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package com.loremind.infrastructure.transfer.pdf;
|
||||||
|
|
||||||
|
import com.loremind.domain.files.ports.FileStorage;
|
||||||
|
import com.loremind.domain.images.ports.ImageStorage;
|
||||||
|
import com.loremind.domain.shared.template.FieldType;
|
||||||
|
import com.loremind.domain.shared.template.TemplateField;
|
||||||
|
import com.loremind.infrastructure.persistence.entity.*;
|
||||||
|
import com.loremind.infrastructure.persistence.jpa.*;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export PDF — couverture des sections RICHES non exercées par {@link PdfExportServiceTest} :
|
||||||
|
* structure narrative (arc → quête → scène), battlemap, lore (pages groupées par chemin de
|
||||||
|
* dossier + champs pilotés par template TEXT/KEY_VALUE/IMAGE/TABLE), portraits, et
|
||||||
|
* statistiques Foundry nettoyées. Le stockage binaire est mocké pour fournir un VRAI PNG
|
||||||
|
* (encode/redimensionnement réellement exécuté) et des cas dégradés (binaire illisible, vidéo).
|
||||||
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@Transactional
|
||||||
|
class PdfExportServiceRichTest {
|
||||||
|
|
||||||
|
@Autowired private PdfExportService service;
|
||||||
|
@Autowired private CampaignJpaRepository campaignRepo;
|
||||||
|
@Autowired private GameSystemJpaRepository gameSystemRepo;
|
||||||
|
@Autowired private ArcJpaRepository arcRepo;
|
||||||
|
@Autowired private ChapterJpaRepository chapterRepo;
|
||||||
|
@Autowired private SceneJpaRepository sceneRepo;
|
||||||
|
@Autowired private NpcJpaRepository npcRepo;
|
||||||
|
@Autowired private EnemyJpaRepository enemyRepo;
|
||||||
|
@Autowired private ImageJpaRepository imageRepo;
|
||||||
|
@Autowired private StoredFileJpaRepository storedFileRepo;
|
||||||
|
@Autowired private LoreJpaRepository loreRepo;
|
||||||
|
@Autowired private LoreNodeJpaRepository loreNodeRepo;
|
||||||
|
@Autowired private PageJpaRepository pageRepo;
|
||||||
|
@Autowired private TemplateJpaRepository templateRepo;
|
||||||
|
|
||||||
|
@MockitoBean private ImageStorage imageStorage;
|
||||||
|
@MockitoBean private FileStorage fileStorage;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exportsRichCampaign_narrativeLorePersonasAndImages() throws Exception {
|
||||||
|
byte[] png = tinyPng();
|
||||||
|
// Image décodable vs binaire corrompu (branche encode -> null).
|
||||||
|
ImageJpaEntity good = imageRepo.save(ImageJpaEntity.builder()
|
||||||
|
.filename("good.png").contentType("image/png").sizeBytes(png.length).storageKey("images/good.png").build());
|
||||||
|
ImageJpaEntity bad = imageRepo.save(ImageJpaEntity.builder()
|
||||||
|
.filename("bad.png").contentType("image/png").sizeBytes(3).storageKey("images/bad.png").build());
|
||||||
|
when(imageStorage.download(eq("images/good.png"))).thenAnswer(inv -> new ByteArrayInputStream(png));
|
||||||
|
when(imageStorage.download(eq("images/bad.png"))).thenAnswer(inv -> new ByteArrayInputStream("xxx".getBytes()));
|
||||||
|
|
||||||
|
// Battlemap image (rendue) vs vidéo (ignorée car non rendable en PDF).
|
||||||
|
StoredFileJpaEntity mapImg = storedFileRepo.save(StoredFileJpaEntity.builder()
|
||||||
|
.filename("map.png").contentType("image/png").sizeBytes(png.length).storageKey("files/map.png").build());
|
||||||
|
StoredFileJpaEntity clip = storedFileRepo.save(StoredFileJpaEntity.builder()
|
||||||
|
.filename("clip.mp4").contentType("video/mp4").sizeBytes(10).storageKey("files/clip.mp4").build());
|
||||||
|
when(fileStorage.download(eq("files/map.png"))).thenAnswer(inv -> new ByteArrayInputStream(png));
|
||||||
|
|
||||||
|
// Système de jeu : templates PNJ (TEXT + KEY_VALUE_LIST + IMAGE) et ennemi (TEXT).
|
||||||
|
GameSystemJpaEntity gs = gameSystemRepo.save(GameSystemJpaEntity.builder()
|
||||||
|
.name("RichSys")
|
||||||
|
.npcTemplate(List.of(
|
||||||
|
new TemplateField("Apparence", FieldType.TEXT),
|
||||||
|
TemplateField.keyValueList("Caractéristiques", List.of("Force", "Dextérité")),
|
||||||
|
new TemplateField("Galerie", FieldType.IMAGE)))
|
||||||
|
.enemyTemplate(List.of(new TemplateField("Tactique", FieldType.TEXT)))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Lore : arbre Géographie / Villes + page pilotée par template (TEXT + TABLE).
|
||||||
|
LoreJpaEntity lore = loreRepo.save(LoreJpaEntity.builder().name("Atlas").build());
|
||||||
|
Long lid = lore.getId();
|
||||||
|
LoreNodeJpaEntity geo = loreNodeRepo.save(LoreNodeJpaEntity.builder().name("Géographie").loreId(lid).order(0).build());
|
||||||
|
LoreNodeJpaEntity villes = loreNodeRepo.save(LoreNodeJpaEntity.builder().name("Villes").loreId(lid).parentId(geo.getId()).order(0).build());
|
||||||
|
TemplateJpaEntity pageTpl = templateRepo.save(TemplateJpaEntity.builder()
|
||||||
|
.loreId(lid).name("Ville")
|
||||||
|
.fields(List.of(
|
||||||
|
new TemplateField("Histoire", FieldType.TEXT),
|
||||||
|
new TemplateField("Commerces", FieldType.TABLE, null, List.of("Nom", "Prix"))))
|
||||||
|
.build());
|
||||||
|
pageRepo.save(PageJpaEntity.builder()
|
||||||
|
.loreId(lid).nodeId(villes.getId()).templateId(pageTpl.getId()).title("Padhrad").order(0)
|
||||||
|
.values(Map.of("Histoire", "Vieille cité portuaire.\nFondée il y a mille ans."))
|
||||||
|
.tableValues(Map.of("Commerces", List.of(Map.of("Nom", "Forge de Korr", "Prix", "10 po"))))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
CampaignJpaEntity camp = campaignRepo.save(CampaignJpaEntity.builder()
|
||||||
|
.name("Campagne Riche & <test>").description("Desc.\nMultiligne.").arcsCount(1)
|
||||||
|
.gameSystemId(String.valueOf(gs.getId())).loreId(String.valueOf(lid)).build());
|
||||||
|
|
||||||
|
// Narration : arc -> quête -> 2 scènes (battlemap image + battlemap vidéo).
|
||||||
|
ArcJpaEntity arc = arcRepo.save(ArcJpaEntity.builder()
|
||||||
|
.name("Acte I").campaignId(camp.getId()).order(0)
|
||||||
|
.description("Le voyage commence.").themes("Trahison").stakes("La cité tombe")
|
||||||
|
.rewards("Trésor").resolution("Victoire").gmNotes("Secret MJ")
|
||||||
|
.illustrationImageIds(List.of(String.valueOf(good.getId()))).build());
|
||||||
|
ChapterJpaEntity chap = chapterRepo.save(ChapterJpaEntity.builder()
|
||||||
|
.name("La porte").arcId(arc.getId()).order(0)
|
||||||
|
.description("Franchir la porte.").playerObjectives("Entrer").narrativeStakes("Le temps presse")
|
||||||
|
.gmNotes("Piège").build());
|
||||||
|
sceneRepo.save(SceneJpaEntity.builder()
|
||||||
|
.name("L'embuscade").chapterId(chap.getId()).order(0)
|
||||||
|
.location("Ruelle").timing("Nuit").atmosphere("Tendue").playerNarration("Des ombres bougent")
|
||||||
|
.gmSecretNotes("3 bandits").choicesConsequences("Fuir ou combattre").combatDifficulty("Moyen")
|
||||||
|
.battlemapMediaFileId(String.valueOf(mapImg.getId())).build());
|
||||||
|
sceneRepo.save(SceneJpaEntity.builder()
|
||||||
|
.name("La poursuite").chapterId(chap.getId()).order(1)
|
||||||
|
.battlemapMediaFileId(String.valueOf(clip.getId())).build());
|
||||||
|
|
||||||
|
// PNJ avec portrait + champs de template (dont galerie d'image corrompue) ; rangé en dossier.
|
||||||
|
npcRepo.save(NpcJpaEntity.builder()
|
||||||
|
.campaignId(camp.getId()).name("Maître Orlin").folder("Padhrad").order(0)
|
||||||
|
.portraitImageId(String.valueOf(good.getId()))
|
||||||
|
.values(Map.of("Apparence", "Vieillard à barbe blanche"))
|
||||||
|
.keyValueValues(Map.of("Caractéristiques", Map.of("Force", "8", "Dextérité", "11")))
|
||||||
|
.imageValues(Map.of("Galerie", List.of(String.valueOf(bad.getId()))))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Ennemi avec stats Foundry : bruit (0/false/rollMode) filtré, clés humanisées.
|
||||||
|
enemyRepo.save(EnemyJpaEntity.builder()
|
||||||
|
.campaignId(camp.getId()).name("Bandit").folder("Pègre").order(0).level("2")
|
||||||
|
.values(Map.of("Tactique", "Embuscade"))
|
||||||
|
.foundryStats(new java.util.LinkedHashMap<>(Map.of(
|
||||||
|
"attributes.hp.value", "25",
|
||||||
|
"attributes.ac.value", "0", // bruit (0) -> masqué
|
||||||
|
"system.details.cr", "3",
|
||||||
|
"flags.core.rollMode", "gmroll" // technique -> masqué
|
||||||
|
)))
|
||||||
|
.build());
|
||||||
|
|
||||||
|
byte[] pdf = service.export(String.valueOf(camp.getId()));
|
||||||
|
|
||||||
|
assertNotNull(pdf);
|
||||||
|
assertTrue(pdf.length > 1000, "le PDF riche doit avoir un contenu substantiel");
|
||||||
|
assertEquals("%PDF-", new String(pdf, 0, 5, StandardCharsets.US_ASCII));
|
||||||
|
assertEquals("Campagne Riche & <test>", service.campaignName(String.valueOf(camp.getId())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void export_unknownCampaign_throws() {
|
||||||
|
assertThrows(java.util.NoSuchElementException.class, () -> service.export("999999999"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Minuscule PNG 4×4 réellement décodable par ImageIO (pour exercer encode()). */
|
||||||
|
private static byte[] tinyPng() throws Exception {
|
||||||
|
BufferedImage img = new BufferedImage(4, 4, BufferedImage.TYPE_INT_RGB);
|
||||||
|
img.setRGB(0, 0, 0x8A7BC8);
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
ImageIO.write(img, "png", out);
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user