client = new InMemoryMangadexClient(); $this->repository = new InMemoryMangaRepository(); $this->service = new MangadxChapterSynchronizationService($this->client, $this->repository); } private function makeManga(string $externalId = 'manga-123'): Manga { return new Manga( new MangaId('manga-id'), new MangaTitle('Test'), new MangaSlug('test'), 'Desc', 'Author', 2024, [], 'ongoing', new ExternalId($externalId) ); } private function chapterEntry(string $number, string $lang, ?string $volume): array { return [ 'attributes' => [ 'chapter' => $number, 'translatedLanguage' => $lang, 'title' => "Chapter $number", 'volume' => $volume, ], ]; } /** * Chapitres sans volume entre deux volumes différents → assignés au volume précédent * * Ch1→Vol1, Ch2→null, Ch3→null, Ch4→Vol2 * Après sync : Ch2 et Ch3 doivent avoir Vol1 */ public function testVolumeTransitionIsAssignedToPreviousVolume(): void { $manga = $this->makeManga(); $this->client->addFeed('manga-123', [ $this->chapterEntry('1', 'en', '1'), $this->chapterEntry('2', 'en', null), $this->chapterEntry('3', 'en', null), $this->chapterEntry('4', 'en', '2'), ]); $this->service->synchronizeChapters($manga); $chapters = $this->indexedByNumber($manga->pullNewChapters()); $this->assertSame(1, $chapters[1.0]->getVolume(), 'Ch1 doit rester Vol1'); $this->assertSame(1, $chapters[2.0]->getVolume(), 'Ch2 (transition) doit être assigné à Vol1'); $this->assertSame(1, $chapters[3.0]->getVolume(), 'Ch3 (transition) doit être assigné à Vol1'); $this->assertSame(2, $chapters[4.0]->getVolume(), 'Ch4 doit rester Vol2'); } /** * Chapitres en début de série sans volume → assignés au premier volume trouvé * * Ch1→null, Ch2→null, Ch3→Vol1 * Après sync : Ch1 et Ch2 doivent avoir Vol1 */ public function testChaptersWithoutVolumeAtStartGetNextVolume(): void { $manga = $this->makeManga(); $this->client->addFeed('manga-123', [ $this->chapterEntry('1', 'en', null), $this->chapterEntry('2', 'en', null), $this->chapterEntry('3', 'en', '1'), ]); $this->service->synchronizeChapters($manga); $chapters = $this->indexedByNumber($manga->pullNewChapters()); $this->assertSame(1, $chapters[1.0]->getVolume(), 'Ch1 (début de série) doit prendre Vol1'); $this->assertSame(1, $chapters[2.0]->getVolume(), 'Ch2 (début de série) doit prendre Vol1'); $this->assertSame(1, $chapters[3.0]->getVolume(), 'Ch3 doit rester Vol1'); } /** * Chapitres avec volumes explicites ne sont pas modifiés * * Ch1→Vol1, Ch2→Vol1, Ch3→Vol2 → inchangé */ public function testChaptersWithExplicitVolumesArePreserved(): void { $manga = $this->makeManga(); $this->client->addFeed('manga-123', [ $this->chapterEntry('1', 'en', '1'), $this->chapterEntry('2', 'en', '1'), $this->chapterEntry('3', 'en', '2'), ]); $this->service->synchronizeChapters($manga); $chapters = $this->indexedByNumber($manga->pullNewChapters()); $this->assertSame(1, $chapters[1.0]->getVolume()); $this->assertSame(1, $chapters[2.0]->getVolume()); $this->assertSame(2, $chapters[3.0]->getVolume()); } /** * La version française est prioritaire sur l'anglaise * * Même chapitre disponible EN (volume 1) et FR (volume 2) → FR gagne */ public function testFrenchChaptersTakePriorityOverEnglish(): void { $manga = $this->makeManga(); $this->client->addFeed('manga-123', [ $this->chapterEntry('1', 'en', '1'), $this->chapterEntry('1', 'fr', '2'), ]); $this->service->synchronizeChapters($manga); $chapters = $this->indexedByNumber($manga->pullNewChapters()); $this->assertCount(1, $chapters, 'Un seul chapitre 1 doit exister'); $this->assertSame(2, $chapters[1.0]->getVolume(), 'La version FR (Vol2) doit prendre la priorité'); } /** * Seuls les nouveaux chapitres sont sauvegardés (pas les doublons) * * Ch1 déjà en DB + Ch2 nouveau → seul Ch2 est retourné */ public function testOnlyNewChaptersAreSaved(): void { $manga = $this->makeManga(); // Pré-peupler la DB avec Ch1 $existingChapter = new Chapter( new ChapterId('existing-uuid'), new MangaId('manga-id'), 1.0, 'Chapter 1', 1, true ); $manga->addChapter($existingChapter); $this->repository->save($manga); // Feed contient Ch1 (déjà en DB) et Ch2 (nouveau) $this->client->addFeed('manga-123', [ $this->chapterEntry('1', 'en', '1'), $this->chapterEntry('2', 'en', '1'), ]); $newChapterIds = $this->service->synchronizeChapters($manga); $this->assertCount(1, $newChapterIds, 'Seul Ch2 doit être retourné comme nouveau'); $newChapters = $manga->pullNewChapters(); $this->assertCount(1, $newChapters); $this->assertSame(2.0, $newChapters[0]->getNumber(), 'Le nouveau chapitre doit être Ch2'); } /** * @param Chapter[] $chapters * @return array */ private function indexedByNumber(array $chapters): array { $result = []; foreach ($chapters as $chapter) { $result[$chapter->getNumber()] = $chapter; } return $result; } }