diff --git a/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js b/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js
index c6f6fcc..11c2ebd 100644
--- a/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js
+++ b/assets/vue/app/domain/manga/infrastructure/api/apiMangaRepository.js
@@ -174,6 +174,25 @@ export class ApiMangaRepository {
}
}
+ async editManga(mangaId, updateData) {
+ try {
+ const response = await fetch(`/api/mangas/${mangaId}/edit`, {
+ method: 'PUT',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(updateData)
+ });
+ if (!response.ok) {
+ throw new Error('Failed to edit manga');
+ }
+ return await response.json();
+ } catch (error) {
+ console.error('API Error:', error);
+ throw error;
+ }
+ }
+
async deleteChapter(chapterId) {
try {
const response = await fetch(`/api/manga/chapters/${chapterId}/cbz`, {
diff --git a/assets/vue/app/domain/manga/presentation/components/MangaEditModal.vue b/assets/vue/app/domain/manga/presentation/components/MangaEditModal.vue
new file mode 100644
index 0000000..d6cbea7
--- /dev/null
+++ b/assets/vue/app/domain/manga/presentation/components/MangaEditModal.vue
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+
diff --git a/assets/vue/app/domain/manga/presentation/composables/useMangaEdit.js b/assets/vue/app/domain/manga/presentation/composables/useMangaEdit.js
new file mode 100644
index 0000000..a35379a
--- /dev/null
+++ b/assets/vue/app/domain/manga/presentation/composables/useMangaEdit.js
@@ -0,0 +1,48 @@
+import { useMutation, useQueryClient } from '@tanstack/vue-query';
+import { ref } from 'vue';
+import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository';
+
+export function useMangaEdit() {
+ const mangaRepository = new ApiMangaRepository();
+ const queryClient = useQueryClient();
+ const isEditModalOpen = ref(false);
+
+ const editMutation = useMutation({
+ mutationFn: ({ mangaId, updateData }) => {
+ return mangaRepository.editManga(mangaId, updateData);
+ },
+ onSuccess: (data, variables) => {
+ // Invalider et refetch les données du manga
+ queryClient.invalidateQueries({ queryKey: ['manga', variables.mangaId] });
+ queryClient.invalidateQueries({ queryKey: ['mangas'] });
+ }
+ });
+
+ const openEditModal = () => {
+ isEditModalOpen.value = true;
+ };
+
+ const closeEditModal = () => {
+ isEditModalOpen.value = false;
+ };
+
+ const editManga = async (mangaId, updateData) => {
+ try {
+ await editMutation.mutateAsync({ mangaId, updateData });
+ closeEditModal();
+ } catch (error) {
+ console.error('Erreur lors de l\'édition du manga:', error);
+ throw error;
+ }
+ };
+
+ return {
+ isEditModalOpen,
+ openEditModal,
+ closeEditModal,
+ editManga,
+ isLoading: editMutation.isPending,
+ error: editMutation.error,
+ isSuccess: editMutation.isSuccess
+ };
+}
diff --git a/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue b/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue
index 3635841..bc00818 100644
--- a/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue
+++ b/assets/vue/app/domain/manga/presentation/pages/MangaDetails.vue
@@ -37,6 +37,16 @@
@close="closePreferredSourcesModal"
@save="savePreferredSources"
/>
+
+
+
@@ -64,10 +74,12 @@ import { computed, onUnmounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useMangaDetails } from '../composables/useMangaDetails';
+import { useMangaEdit } from '../composables/useMangaEdit';
import { useMangaPreferredSources } from '../composables/useMangaPreferredSources';
import { useMangaVolumes } from '../composables/useMangaVolumes';
- import MangaHeader from '../components/MangaHeader.vue';
+ import MangaEditModal from '../components/MangaEditModal.vue';
+import MangaHeader from '../components/MangaHeader.vue';
import MangaPreferredSourcesModal from '../components/MangaPreferredSourcesModal.vue';
import MangaVolumeList from '../components/MangaVolumeList.vue';
import MercureListener from '../components/MercureListener.vue';
@@ -105,6 +117,16 @@ import { useMangaStore } from '../../application/store/mangaStore';
savePreferredSources: saveSourcesOrder
} = useMangaPreferredSources(mangaId);
+ // Composable pour l'édition des mangas
+ const {
+ isEditModalOpen,
+ openEditModal,
+ closeEditModal,
+ editManga,
+ isLoading: isEditLoading,
+ error: editError
+ } = useMangaEdit();
+
// Charger les chapitres dans le store quand le manga est chargé
watch(
mangaId,
@@ -133,6 +155,15 @@ import { useMangaStore } from '../../application/store/mangaStore';
}
};
+ // Fonction pour sauvegarder l'édition du manga
+ const saveMangaEdit = async (updateData) => {
+ try {
+ await editManga(mangaId.value, updateData);
+ } catch (error) {
+ console.error('Erreur lors de l\'édition du manga:', error);
+ }
+ };
+
const toolbarConfig = computed(() => ({
leftSection: [
{
@@ -171,7 +202,7 @@ import { useMangaStore } from '../../application/store/mangaStore';
icon: WrenchIcon,
label: 'Edit',
type: 'button',
- onClick: () => console.log('Edit')
+ onClick: openEditModal
},
{
icon: TrashIcon,
diff --git a/public/api-docs.json b/public/api-docs.json
index f605519..b5dc0fb 100644
--- a/public/api-docs.json
+++ b/public/api-docs.json
@@ -1841,6 +1841,97 @@
},
"parameters": []
},
+ "/api/mangas/{id}/edit": {
+ "put": {
+ "operationId": "api_mangas_idedit_put",
+ "tags": [
+ "Manga"
+ ],
+ "responses": {
+ "200": {
+ "description": "Manga resource updated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Manga"
+ }
+ },
+ "application/ld+json": {
+ "schema": {
+ "$ref": "#/components/schemas/Manga.jsonld"
+ }
+ },
+ "text/html": {
+ "schema": {
+ "$ref": "#/components/schemas/Manga"
+ }
+ },
+ "application/hal+json": {
+ "schema": {
+ "$ref": "#/components/schemas/Manga.jsonhal"
+ }
+ }
+ },
+ "links": {}
+ },
+ "400": {
+ "description": "Invalid input"
+ },
+ "422": {
+ "description": "Unprocessable entity"
+ },
+ "404": {
+ "description": "Resource not found"
+ }
+ },
+ "summary": "Edit an existing manga",
+ "description": "Updates an existing manga with provided data (partial update supported)",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "description": "EditMangaResource identifier",
+ "required": true,
+ "deprecated": false,
+ "allowEmptyValue": false,
+ "schema": {
+ "type": "string"
+ },
+ "style": "simple",
+ "explode": false,
+ "allowReserved": false
+ }
+ ],
+ "requestBody": {
+ "description": "The updated Manga resource",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Manga"
+ }
+ },
+ "application/ld+json": {
+ "schema": {
+ "$ref": "#/components/schemas/Manga.jsonld"
+ }
+ },
+ "text/html": {
+ "schema": {
+ "$ref": "#/components/schemas/Manga"
+ }
+ },
+ "application/hal+json": {
+ "schema": {
+ "$ref": "#/components/schemas/Manga.jsonhal"
+ }
+ }
+ },
+ "required": true
+ },
+ "deprecated": false
+ },
+ "parameters": []
+ },
"/api/mangas/{id}/preferred-sources": {
"get": {
"operationId": "api_mangas_idpreferred-sources_get",
@@ -3397,6 +3488,12 @@
"null"
]
},
+ "thumbnailUrl": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"rating": {
"type": [
"number",
@@ -3463,6 +3560,12 @@
"null"
]
},
+ "thumbnailUrl": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"rating": {
"type": [
"number",
@@ -3550,6 +3653,12 @@
"null"
]
},
+ "thumbnailUrl": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
"rating": {
"type": [
"number",
diff --git a/src/Domain/Manga/Application/Command/EditManga.php b/src/Domain/Manga/Application/Command/EditManga.php
new file mode 100644
index 0000000..2fb009d
--- /dev/null
+++ b/src/Domain/Manga/Application/Command/EditManga.php
@@ -0,0 +1,18 @@
+status,
$command->externalId ? new ExternalId($command->externalId) : null,
$command->imageUrl,
- $command->rating
+ $command->rating,
+ null, // imageUrls
+ [], // alternativeSlugs
);
if (!is_null($command->imageUrl)) {
@@ -55,4 +57,4 @@ readonly class CreateMangaHandler
$this->messageBus->dispatch(new MangaCreated($command->externalId));
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Domain/Manga/Application/CommandHandler/EditMangaHandler.php b/src/Domain/Manga/Application/CommandHandler/EditMangaHandler.php
new file mode 100644
index 0000000..3a194e5
--- /dev/null
+++ b/src/Domain/Manga/Application/CommandHandler/EditMangaHandler.php
@@ -0,0 +1,59 @@
+mangaRepository->findById($command->id);
+
+ if (!$manga) {
+ throw new MangaNotFoundException($command->id);
+ }
+
+ // Update only provided fields (partial update)
+ if ($command->title !== null) {
+ $manga->updateTitle(new MangaTitle($command->title));
+ }
+
+ if ($command->description !== null) {
+ $manga->updateDescription($command->description);
+ }
+
+ if ($command->author !== null) {
+ $manga->updateAuthor($command->author);
+ }
+
+ if ($command->publicationYear !== null) {
+ $manga->updatePublicationYear($command->publicationYear);
+ }
+
+ if ($command->genres !== null) {
+ $manga->updateGenres($command->genres);
+ }
+
+ if ($command->status !== null) {
+ $manga->updateStatus($command->status);
+ }
+
+ if ($command->rating !== null) {
+ $manga->setRating($command->rating);
+ }
+
+ if ($command->alternativeSlugs !== null) {
+ $manga->updateAlternativeSlugs($command->alternativeSlugs);
+ }
+
+ $this->mangaRepository->save($manga);
+ }
+}
diff --git a/src/Domain/Manga/Application/QueryHandler/GetMangaByIdHandler.php b/src/Domain/Manga/Application/QueryHandler/GetMangaByIdHandler.php
index 7e7386b..1c1e632 100644
--- a/src/Domain/Manga/Application/QueryHandler/GetMangaByIdHandler.php
+++ b/src/Domain/Manga/Application/QueryHandler/GetMangaByIdHandler.php
@@ -25,6 +25,7 @@ readonly class GetMangaByIdHandler
id: $manga->getId()->getValue(),
title: $manga->getTitle()->getValue(),
slug: $manga->getSlug()->getValue(),
+ alternativeSlugs: $manga->getAlternativeSlugs(),
description: $manga->getDescription(),
author: $manga->getAuthor(),
publicationYear: $manga->getPublicationYear(),
@@ -32,7 +33,7 @@ readonly class GetMangaByIdHandler
status: $manga->getStatus(),
externalId: $manga->getExternalId()?->getValue(),
imageUrl: $manga->getImageUrl(),
- thumbnailUrl: $manga->getImageUrls()->getThumbnail(),
+ thumbnailUrl: $manga->getImageUrls()?->getThumbnail(),
rating: $manga->getRating()
);
}
diff --git a/src/Domain/Manga/Application/Response/MangaResponse.php b/src/Domain/Manga/Application/Response/MangaResponse.php
index 8c61833..d3d7ced 100644
--- a/src/Domain/Manga/Application/Response/MangaResponse.php
+++ b/src/Domain/Manga/Application/Response/MangaResponse.php
@@ -8,6 +8,7 @@ readonly class MangaResponse
public string $id,
public string $title,
public string $slug,
+ public array $alternativeSlugs,
public string $description,
public string $author,
public int $publicationYear,
diff --git a/src/Domain/Manga/Domain/Model/Manga.php b/src/Domain/Manga/Domain/Model/Manga.php
index bfd0786..8d9fef0 100644
--- a/src/Domain/Manga/Domain/Model/Manga.php
+++ b/src/Domain/Manga/Domain/Model/Manga.php
@@ -24,6 +24,7 @@ final class Manga
private ?string $imageUrl = null,
private ?float $rating = null,
private ?ImageUrls $imageUrls = null,
+ private array $alternativeSlugs = [],
private ?DateTimeImmutable $createdAt = null,
) {}
@@ -102,8 +103,48 @@ final class Manga
$this->imageUrls = $imageUrls;
}
+ public function getAlternativeSlugs(): array
+ {
+ return $this->alternativeSlugs;
+ }
+
+ public function updateTitle(MangaTitle $title): void
+ {
+ $this->title = $title;
+ }
+
+ public function updateDescription(string $description): void
+ {
+ $this->description = $description;
+ }
+
+ public function updateAuthor(string $author): void
+ {
+ $this->author = $author;
+ }
+
+ public function updatePublicationYear(int $publicationYear): void
+ {
+ $this->publicationYear = $publicationYear;
+ }
+
+ public function updateGenres(array $genres): void
+ {
+ $this->genres = $genres;
+ }
+
+ public function updateStatus(string $status): void
+ {
+ $this->status = $status;
+ }
+
+ public function updateAlternativeSlugs(array $alternativeSlugs): void
+ {
+ $this->alternativeSlugs = $alternativeSlugs;
+ }
+
public function getCreatedAt(): ?DateTimeImmutable
{
return $this->createdAt;
}
-}
\ No newline at end of file
+}
diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/Dto/MangaDetail.php b/src/Domain/Manga/Infrastructure/ApiPlatform/Dto/MangaDetail.php
index 748f551..25b1d1c 100644
--- a/src/Domain/Manga/Infrastructure/ApiPlatform/Dto/MangaDetail.php
+++ b/src/Domain/Manga/Infrastructure/ApiPlatform/Dto/MangaDetail.php
@@ -11,6 +11,7 @@ readonly class MangaDetail
public string $id,
public string $title,
public string $slug,
+ public array $alternativeSlugs,
public string $description,
public string $author,
public int $publicationYear,
diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/Resource/EditMangaResource.php b/src/Domain/Manga/Infrastructure/ApiPlatform/Resource/EditMangaResource.php
new file mode 100644
index 0000000..292842c
--- /dev/null
+++ b/src/Domain/Manga/Infrastructure/ApiPlatform/Resource/EditMangaResource.php
@@ -0,0 +1,55 @@
+ 'Edit an existing manga',
+ 'description' => 'Updates an existing manga with provided data (partial update supported)'
+ ]
+ )
+ ]
+)]
+class EditMangaResource
+{
+ #[Assert\Length(min: 1, max: 255, minMessage: 'Le titre doit contenir au moins {{ limit }} caractère', maxMessage: 'Le titre ne peut pas dépasser {{ limit }} caractères')]
+ public ?string $title = null;
+
+ public ?string $description = null;
+
+ public ?string $author = null;
+
+ #[Assert\Type(type: 'integer', message: 'L\'année de publication doit être un nombre entier')]
+ #[Assert\Range(min: 1900, max: 2100, notInRangeMessage: 'L\'année de publication doit être comprise entre {{ min }} et {{ max }}')]
+ public ?int $publicationYear = null;
+
+ #[Assert\Type(type: 'array', message: 'Les genres doivent être une liste')]
+ #[Assert\Count(min: 1, minMessage: 'Vous devez spécifier au moins un genre')]
+ public ?array $genres = null;
+
+ #[Assert\Choice(choices: ['ongoing', 'completed', 'hiatus'], message: 'Le statut doit être l\'un des suivants : ongoing, completed, hiatus')]
+ public ?string $status = null;
+
+ #[Assert\Type(type: 'float', message: 'La note doit être un nombre décimal')]
+ #[Assert\Range(min: 0, max: 10, notInRangeMessage: 'La note doit être comprise entre {{ min }} et {{ max }}')]
+ public ?float $rating = null;
+
+ #[Assert\Type(type: 'array', message: 'Les slugs alternatifs doivent être une liste')]
+ #[Assert\All([
+ new Assert\Type('string'),
+ new Assert\Regex(pattern: '/^[a-z0-9-]+$/', message: 'Chaque slug alternatif ne peut contenir que des lettres minuscules, des chiffres et des tirets')
+ ])]
+ public ?array $alternativeSlugs = null;
+}
diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Processor/EditMangaProcessor.php b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Processor/EditMangaProcessor.php
new file mode 100644
index 0000000..8ce8ea0
--- /dev/null
+++ b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Processor/EditMangaProcessor.php
@@ -0,0 +1,48 @@
+title,
+ description: $data->description,
+ author: $data->author,
+ publicationYear: $data->publicationYear,
+ genres: $data->genres,
+ status: $data->status,
+ rating: $data->rating,
+ alternativeSlugs: $data->alternativeSlugs ?? []
+ );
+
+ try {
+ $this->handler->handle($command);
+ } catch (MangaNotFoundException $e) {
+ throw new NotFoundHttpException($e->getMessage());
+ }
+ }
+}
diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaBySlugStateProvider.php b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaBySlugStateProvider.php
index fc6845b..22484bd 100644
--- a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaBySlugStateProvider.php
+++ b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaBySlugStateProvider.php
@@ -30,7 +30,8 @@ readonly class GetMangaBySlugStateProvider implements ProviderInterface
status: $response->status,
externalId: $response->externalId,
imageUrl: $response->imageUrl,
+ thumbnailUrl: $response->thumbnailUrl,
rating: $response->rating
);
}
-}
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaStateProvider.php b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaStateProvider.php
index 9741a5f..8e91900 100644
--- a/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaStateProvider.php
+++ b/src/Domain/Manga/Infrastructure/ApiPlatform/State/Provider/GetMangaStateProvider.php
@@ -23,6 +23,7 @@ readonly class GetMangaStateProvider implements ProviderInterface
id: $response->id,
title: $response->title,
slug: $response->slug,
+ alternativeSlugs: $response->alternativeSlugs,
description: $response->description,
author: $response->author,
publicationYear: $response->publicationYear,
diff --git a/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php b/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php
index 2d5273b..f1db758 100644
--- a/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php
+++ b/src/Domain/Manga/Infrastructure/Persistence/LegacyMangaRepository.php
@@ -63,9 +63,17 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
return $entity ? $this->toDomain($entity) : null;
}
- public function save(DomainManga $manga): void
+ public function save(DomainManga $manga): void
{
- $entity = new EntityManga();
+ // Check if this is an update (manga has a numeric ID) or a new creation
+ $entity = null;
+ if ($manga->getId() && $manga->getId()->getValue() && is_numeric($manga->getId()->getValue())) {
+ $entity = $this->entityManager->find(EntityManga::class, (int) $manga->getId()->getValue());
+ }
+
+ if (!$entity) {
+ $entity = new EntityManga();
+ }
$imageUrls = $manga->getImageUrls();
$fullImageUrl = $imageUrls?->getFull();
@@ -78,10 +86,19 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
->setPublicationYear($manga->getPublicationYear())
->setGenres($manga->getGenres())
->setStatus($manga->getStatus())
- ->setExternalId($manga->getExternalId()->getValue())
->setImageUrl($fullImageUrl ?? null)
->setThumbnailUrl($thumbnailUrl ?? null)
- ->setMonitored(false);
+ ->setAlternativeSlugs($manga->getAlternativeSlugs());
+
+ // Only set externalId if it exists (to avoid setting null on update)
+ if ($manga->getExternalId()) {
+ $entity->setExternalId($manga->getExternalId()->getValue());
+ }
+
+ // Only set monitored for new entities
+ if (!$entity->getId()) {
+ $entity->setMonitored(false);
+ }
if ($manga->getRating() !== null) {
$entity->setRating($manga->getRating());
@@ -239,6 +256,7 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
imageUrl: $entity->getImageUrl(),
rating: $entity->getRating(),
imageUrls: $entity->getImageUrl() ? new ImageUrls($entity->getImageUrl() ?? '', $entity->getThumbnailUrl() ?? '') : null,
+ alternativeSlugs: $entity->getAlternativeSlugs() ?? [],
createdAt: $entity->getCreatedAt(),
);
}
diff --git a/tests/Domain/Manga/Application/CommandHandler/EditMangaHandlerTest.php b/tests/Domain/Manga/Application/CommandHandler/EditMangaHandlerTest.php
new file mode 100644
index 0000000..24ee44e
--- /dev/null
+++ b/tests/Domain/Manga/Application/CommandHandler/EditMangaHandlerTest.php
@@ -0,0 +1,170 @@
+repository = new InMemoryMangaRepository();
+ $this->handler = new EditMangaHandler($this->repository);
+ }
+
+ public function testHandleEditMangaSuccess(): void
+ {
+ // Given - Create a manga first
+ $manga = new Manga(
+ new MangaId('manga-123'),
+ new MangaTitle('One Piece'),
+ new MangaSlug('one-piece'),
+ 'Original description',
+ 'Eiichiro Oda',
+ 1997,
+ ['action', 'adventure'],
+ 'ongoing',
+ new ExternalId('external-123'),
+ 'http://example.com/image.jpg',
+ 4.5,
+ null,
+ ['op']
+ );
+
+ $this->repository->save($manga);
+
+ // When - Edit the manga
+ $command = new EditManga(
+ id: 'manga-123',
+ title: 'One Piece Updated',
+ description: 'Updated description',
+ author: 'Eiichiro Oda Updated',
+ publicationYear: 1998,
+ genres: ['action', 'adventure', 'comedy'],
+ status: 'completed',
+ rating: 4.8,
+ alternativeSlugs: ['onepiece', 'op', 'luffy']
+ );
+
+ $this->handler->handle($command);
+
+ // Then - Verify the manga was updated
+ $updatedManga = $this->repository->findById('manga-123');
+ $this->assertNotNull($updatedManga);
+ $this->assertEquals('One Piece Updated', $updatedManga->getTitle()->getValue());
+ $this->assertEquals('Updated description', $updatedManga->getDescription());
+ $this->assertEquals('Eiichiro Oda Updated', $updatedManga->getAuthor());
+ $this->assertEquals(1998, $updatedManga->getPublicationYear());
+ $this->assertEquals(['action', 'adventure', 'comedy'], $updatedManga->getGenres());
+ $this->assertEquals('completed', $updatedManga->getStatus());
+ $this->assertEquals(4.8, $updatedManga->getRating());
+ $this->assertEquals(['onepiece', 'op', 'luffy'], $updatedManga->getAlternativeSlugs());
+ }
+
+ public function testHandleEditMangaPartialUpdate(): void
+ {
+ // Given - Create a manga first
+ $manga = new Manga(
+ new MangaId('manga-123'),
+ new MangaTitle('One Piece'),
+ new MangaSlug('one-piece'),
+ 'Original description',
+ 'Eiichiro Oda',
+ 1997,
+ ['action', 'adventure'],
+ 'ongoing',
+ new ExternalId('external-123'),
+ 'http://example.com/image.jpg',
+ 4.5,
+ null,
+ ['op']
+ );
+
+ $this->repository->save($manga);
+
+ // When - Edit only title and rating
+ $command = new EditManga(
+ id: 'manga-123',
+ title: 'One Piece - Updated Title Only',
+ rating: 4.9
+ );
+
+ $this->handler->handle($command);
+
+ // Then - Verify only specified fields were updated
+ $updatedManga = $this->repository->findById('manga-123');
+ $this->assertNotNull($updatedManga);
+ $this->assertEquals('One Piece - Updated Title Only', $updatedManga->getTitle()->getValue());
+ $this->assertEquals(4.9, $updatedManga->getRating());
+ // Original values should remain unchanged
+ $this->assertEquals('Original description', $updatedManga->getDescription());
+ $this->assertEquals('Eiichiro Oda', $updatedManga->getAuthor());
+ $this->assertEquals(1997, $updatedManga->getPublicationYear());
+ $this->assertEquals(['action', 'adventure'], $updatedManga->getGenres());
+ $this->assertEquals('ongoing', $updatedManga->getStatus());
+ $this->assertEquals(['op'], $updatedManga->getAlternativeSlugs());
+ }
+
+ public function testHandleEditMangaNotFound(): void
+ {
+ // When - Try to edit non-existent manga
+ $command = new EditManga(
+ id: 'non-existent-id',
+ title: 'Updated Title'
+ );
+
+ // Then
+ $this->expectException(MangaNotFoundException::class);
+ $this->handler->handle($command);
+ }
+
+ public function testHandleEditAlternativeSlugsSeparately(): void
+ {
+ // Given - Create a manga first
+ $manga = new Manga(
+ new MangaId('manga-123'),
+ new MangaTitle('One Piece'),
+ new MangaSlug('one-piece'),
+ 'Original description',
+ 'Eiichiro Oda',
+ 1997,
+ ['action', 'adventure'],
+ 'ongoing',
+ new ExternalId('external-123'),
+ 'http://example.com/image.jpg',
+ 4.5,
+ null,
+ ['op', 'onepiece']
+ );
+
+ $this->repository->save($manga);
+
+ // When - Edit only alternativeSlugs
+ $command = new EditManga(
+ id: 'manga-123',
+ alternativeSlugs: ['luffy-manga', 'pirate-king', 'one-piece-manga']
+ );
+
+ $this->handler->handle($command);
+
+ // Then - Verify only alternativeSlugs was updated
+ $updatedManga = $this->repository->findById('manga-123');
+ $this->assertNotNull($updatedManga);
+ $this->assertEquals(['luffy-manga', 'pirate-king', 'one-piece-manga'], $updatedManga->getAlternativeSlugs());
+ // All other fields should remain unchanged
+ $this->assertEquals('One Piece', $updatedManga->getTitle()->getValue());
+ $this->assertEquals('Original description', $updatedManga->getDescription());
+ }
+}
diff --git a/tests/Feature/Manga/EditMangaTest.php b/tests/Feature/Manga/EditMangaTest.php
new file mode 100644
index 0000000..d3fb19c
--- /dev/null
+++ b/tests/Feature/Manga/EditMangaTest.php
@@ -0,0 +1,174 @@
+request('POST', '/api/mangas/create', [
+ 'json' => [
+ 'title' => 'One Piece',
+ 'slug' => 'one-piece',
+ 'description' => 'Original description',
+ 'author' => 'Eiichiro Oda',
+ 'publicationYear' => 1997,
+ 'genres' => ['action', 'adventure'],
+ 'status' => 'ongoing',
+ 'externalId' => 'external-123',
+ 'imageUrl' => 'http://example.com/image.jpg',
+ 'rating' => 4.5
+ ]
+ ]);
+
+ $this->assertResponseIsSuccessful();
+
+ // Get the created manga ID from database
+ $entityManager = static::getContainer()->get('doctrine')->getManager();
+ $createdManga = $entityManager->getRepository(\App\Entity\Manga::class)->findOneBy(['slug' => 'one-piece']);
+ $this->assertNotNull($createdManga);
+ $mangaId = $createdManga->getId();
+
+ // When - Edit the manga
+ $response = $client->request('PUT', '/api/mangas/' . $mangaId . '/edit', [
+ 'json' => [
+ 'title' => 'One Piece Updated',
+ 'description' => 'Updated description',
+ 'author' => 'Eiichiro Oda Updated',
+ 'publicationYear' => 1998,
+ 'genres' => ['action', 'adventure', 'comedy'],
+ 'status' => 'completed',
+ 'rating' => 4.8,
+ 'alternativeSlugs' => ['onepiece', 'op']
+ ]
+ ]);
+
+ // Then
+ $this->assertResponseIsSuccessful();
+
+ // Verify the manga was updated in database
+ $entityManager = static::getContainer()->get('doctrine')->getManager();
+ $manga = $entityManager->getRepository(\App\Entity\Manga::class)->find($mangaId);
+
+ $this->assertNotNull($manga);
+ $this->assertEquals('One Piece Updated', $manga->getTitle());
+ $this->assertEquals('Updated description', $manga->getDescription());
+ $this->assertEquals('Eiichiro Oda Updated', $manga->getAuthor());
+ $this->assertEquals(1998, $manga->getPublicationYear());
+ $this->assertEquals(['action', 'adventure', 'comedy'], $manga->getGenres());
+ $this->assertEquals('completed', $manga->getStatus());
+ $this->assertEquals(4.8, $manga->getRating());
+ $this->assertEquals(['onepiece', 'op'], $manga->getAlternativeSlugs());
+ }
+
+ public function testEditMangaWithInvalidData(): void
+ {
+ // Given - Create a manga first
+ $client = static::createClient();
+ $response = $client->request('POST', '/api/mangas/create', [
+ 'json' => [
+ 'title' => 'One Piece 2',
+ 'slug' => 'one-piece-2',
+ 'description' => 'Original description',
+ 'author' => 'Eiichiro Oda',
+ 'publicationYear' => 1997,
+ 'genres' => ['action', 'adventure'],
+ 'status' => 'ongoing'
+ ]
+ ]);
+
+ $this->assertResponseIsSuccessful();
+
+ // Get the created manga ID from database
+ $entityManager = static::getContainer()->get('doctrine')->getManager();
+ $createdManga = $entityManager->getRepository(\App\Entity\Manga::class)->findOneBy(['slug' => 'one-piece-2']);
+ $this->assertNotNull($createdManga);
+ $mangaId = $createdManga->getId();
+
+ // When - Try to edit with invalid data
+ $client->request('PUT', '/api/mangas/' . $mangaId . '/edit', [
+ 'json' => [
+ 'title' => '', // Invalid: empty title
+ 'publicationYear' => 2200, // Invalid: year > 2100
+ 'genres' => [], // Invalid: empty genres
+ 'status' => 'invalid-status', // Invalid status
+ 'rating' => 6.0 // Invalid: rating > 5
+ ]
+ ]);
+
+ // Then
+ $this->assertResponseStatusCodeSame(422);
+ }
+
+ public function testEditMangaNotFound(): void
+ {
+ // When - Try to edit non-existent manga
+ $client = static::createClient();
+ $client->request('PUT', '/api/mangas/9999999/edit', [
+ 'json' => [
+ 'title' => 'Updated Title'
+ ]
+ ]);
+
+ // Then
+ $this->assertResponseStatusCodeSame(404);
+ }
+
+ public function testEditMangaPartialUpdate(): void
+ {
+ // Given - Create a manga first
+ $client = static::createClient();
+ $response = $client->request('POST', '/api/mangas/create', [
+ 'json' => [
+ 'title' => 'One Piece 3',
+ 'slug' => 'one-piece-3',
+ 'description' => 'Original description',
+ 'author' => 'Eiichiro Oda',
+ 'publicationYear' => 1997,
+ 'genres' => ['action', 'adventure'],
+ 'status' => 'ongoing',
+ 'rating' => 4.5
+ ]
+ ]);
+
+ $this->assertResponseIsSuccessful();
+
+ // Get the created manga ID from database
+ $entityManager = static::getContainer()->get('doctrine')->getManager();
+ $createdManga = $entityManager->getRepository(\App\Entity\Manga::class)->findOneBy(['slug' => 'one-piece-3']);
+ $this->assertNotNull($createdManga);
+ $mangaId = $createdManga->getId();
+
+ // When - Edit only title and rating
+ $client->request('PUT', '/api/mangas/' . $mangaId . '/edit', [
+ 'json' => [
+ 'title' => 'One Piece - Updated Title Only',
+ 'rating' => 4.9
+ ]
+ ]);
+
+ // Then
+ $this->assertResponseIsSuccessful();
+
+ // Verify only specified fields were updated
+ $entityManager = static::getContainer()->get('doctrine')->getManager();
+ $manga = $entityManager->getRepository(\App\Entity\Manga::class)->find($mangaId);
+
+ $this->assertNotNull($manga);
+ $this->assertEquals('One Piece - Updated Title Only', $manga->getTitle());
+ $this->assertEquals(4.9, $manga->getRating());
+ // Original values should remain unchanged
+ $this->assertEquals('Original description', $manga->getDescription());
+ $this->assertEquals('Eiichiro Oda', $manga->getAuthor());
+ $this->assertEquals(1997, $manga->getPublicationYear());
+ $this->assertEquals(['action', 'adventure'], $manga->getGenres());
+ $this->assertEquals('ongoing', $manga->getStatus());
+ }
+}
diff --git a/tests/Feature/Reader/GetChapterContextTest.php b/tests/Feature/Reader/GetChapterContextTest.php
index 9cc7c0c..d897a3d 100644
--- a/tests/Feature/Reader/GetChapterContextTest.php
+++ b/tests/Feature/Reader/GetChapterContextTest.php
@@ -24,18 +24,24 @@ final class GetChapterContextTest extends AbstractApiTestCase
'manga' => $manga,
'title' => 'Chapter 1',
'number' => 1,
+ 'visible' => true,
+ 'cbzPath' => '/path/to/chapter1.cbz',
]);
$chapter2 = ChapterFactory::createOne([
'manga' => $manga,
'title' => 'Chapter 2',
'number' => 2,
+ 'visible' => true,
+ 'cbzPath' => '/path/to/chapter2.cbz',
]);
$chapter3 = ChapterFactory::createOne([
'manga' => $manga,
'title' => 'Chapter 3',
'number' => 3,
+ 'visible' => true,
+ 'cbzPath' => '/path/to/chapter3.cbz',
]);
// Act