feat: ajout de la fonctionnalité d'édition des mangas, incluant la création d'un modal d'édition, la mise à jour de l'API pour gérer les modifications, et l'intégration de la logique de gestion des erreurs. Tests ajoutés pour valider le bon fonctionnement de l'édition.
This commit is contained in:
parent
896c57ac34
commit
9255509042
@@ -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`, {
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<TransitionRoot as="template" :show="isOpen">
|
||||
<Dialog as="div" class="relative z-50" @close="closeModal">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
</TransitionChild>
|
||||
|
||||
<div class="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<TransitionChild
|
||||
as="template"
|
||||
enter="ease-out duration-300"
|
||||
enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leave-from="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<DialogPanel class="relative transform overflow-hidden rounded-lg bg-white px-6 pb-6 pt-6 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-4xl">
|
||||
<div class="mb-6">
|
||||
<DialogTitle as="h3" class="text-lg font-semibold leading-6 text-gray-900">
|
||||
Edit Manga
|
||||
</DialogTitle>
|
||||
</div>
|
||||
|
||||
<!-- Error state -->
|
||||
<div v-if="error" class="mb-6 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
|
||||
{{ error.message || 'Une erreur est survenue lors de la sauvegarde.' }}
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form @submit.prevent="saveChanges" class="space-y-6">
|
||||
<!-- Titre et Slug -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">Titre</label>
|
||||
<input
|
||||
id="title"
|
||||
v-model="formData.title"
|
||||
type="text"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="Titre du manga"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="slug" class="block text-sm font-medium text-gray-700 mb-2">Slug</label>
|
||||
<input
|
||||
id="slug"
|
||||
:value="manga?.slug || ''"
|
||||
type="text"
|
||||
disabled
|
||||
class="block w-full rounded-md border-gray-300 bg-gray-50 shadow-sm sm:text-sm text-gray-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Année de publication -->
|
||||
<div>
|
||||
<label for="publicationYear" class="block text-sm font-medium text-gray-700 mb-2">Année de publication</label>
|
||||
<input
|
||||
id="publicationYear"
|
||||
v-model.number="formData.publicationYear"
|
||||
type="number"
|
||||
min="1900"
|
||||
:max="new Date().getFullYear()"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="2023"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">Description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
v-model="formData.description"
|
||||
rows="4"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="Description du manga"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Auteur et Statut -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="author" class="block text-sm font-medium text-gray-700 mb-2">Auteur</label>
|
||||
<input
|
||||
id="author"
|
||||
v-model="formData.author"
|
||||
type="text"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="Auteur du manga"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="status" class="block text-sm font-medium text-gray-700 mb-2">Statut</label>
|
||||
<input
|
||||
id="status"
|
||||
v-model="formData.status"
|
||||
type="text"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="ongoing"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note -->
|
||||
<div>
|
||||
<label for="rating" class="block text-sm font-medium text-gray-700 mb-2">Note</label>
|
||||
<input
|
||||
id="rating"
|
||||
v-model.number="formData.rating"
|
||||
type="number"
|
||||
min="0"
|
||||
max="10"
|
||||
step="0.001"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="9.541"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Slugs alternatifs -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Slugs alternatifs</label>
|
||||
<div class="space-y-2">
|
||||
<div v-if="formData.alternativeSlugs.length > 0" class="flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="(slug, index) in formData.alternativeSlugs"
|
||||
:key="index"
|
||||
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"
|
||||
>
|
||||
{{ slug }}
|
||||
<button
|
||||
type="button"
|
||||
@click="removeAlternativeSlug(index)"
|
||||
class="ml-1.5 inline-flex items-center justify-center w-4 h-4 rounded-full text-green-400 hover:text-green-600"
|
||||
>
|
||||
<XMarkIcon class="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="showAlternativeSlugInput = !showAlternativeSlugInput"
|
||||
class="text-green-600 hover:text-green-700 text-sm font-medium"
|
||||
>
|
||||
+ Ajouter un slug alternatif
|
||||
</button>
|
||||
<div v-if="showAlternativeSlugInput" class="flex gap-2">
|
||||
<input
|
||||
v-model="newAlternativeSlug"
|
||||
type="text"
|
||||
class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="Nouveau slug alternatif"
|
||||
@keyup.enter="addAlternativeSlug"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="addAlternativeSlug"
|
||||
class="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm"
|
||||
>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Genres -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Genres</label>
|
||||
<div class="space-y-3">
|
||||
<div v-if="formData.genres.length > 0" class="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
<span
|
||||
v-for="(genre, index) in formData.genres"
|
||||
:key="index"
|
||||
class="inline-flex items-center justify-between px-3 py-1 rounded-md text-sm font-medium bg-gray-100 text-gray-800"
|
||||
>
|
||||
{{ genre }}
|
||||
<button
|
||||
type="button"
|
||||
@click="removeGenre(index)"
|
||||
class="ml-2 inline-flex items-center justify-center w-4 h-4 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<XMarkIcon class="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@click="showGenreInput = !showGenreInput"
|
||||
class="text-green-600 hover:text-green-700 text-sm font-medium"
|
||||
>
|
||||
+ Ajouter un genre
|
||||
</button>
|
||||
<div v-if="showGenreInput" class="flex gap-2">
|
||||
<input
|
||||
v-model="newGenre"
|
||||
type="text"
|
||||
class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
|
||||
placeholder="Nouveau genre"
|
||||
@keyup.enter="addGenre"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@click="addGenre"
|
||||
class="px-3 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 text-sm"
|
||||
>
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Boutons -->
|
||||
<div class="mt-8 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
@click="closeModal"
|
||||
:disabled="isSaving"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:disabled="isSaving"
|
||||
@click="saveChanges"
|
||||
>
|
||||
<div v-if="isSaving" class="flex items-center">
|
||||
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Saving...
|
||||
</div>
|
||||
<span v-else>Save</span>
|
||||
</button>
|
||||
</div>
|
||||
</DialogPanel>
|
||||
</TransitionChild>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</TransitionRoot>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue';
|
||||
import { XMarkIcon } from '@heroicons/vue/24/outline';
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
isOpen: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
manga: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
isSaving: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close', 'save']);
|
||||
|
||||
// Données du formulaire
|
||||
const formData = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
author: '',
|
||||
publicationYear: null,
|
||||
status: '',
|
||||
rating: null,
|
||||
genres: [],
|
||||
alternativeSlugs: []
|
||||
});
|
||||
|
||||
// Contrôle de l'affichage des inputs
|
||||
const showGenreInput = ref(false);
|
||||
const showAlternativeSlugInput = ref(false);
|
||||
|
||||
// Champs temporaires pour ajouter des genres et slugs
|
||||
const newGenre = ref('');
|
||||
const newAlternativeSlug = ref('');
|
||||
|
||||
// Initialiser le formulaire avec les données du manga
|
||||
watch(() => props.manga, (newManga) => {
|
||||
if (newManga) {
|
||||
formData.value = {
|
||||
title: newManga.title || '',
|
||||
description: newManga.description || '',
|
||||
author: newManga.author || '',
|
||||
publicationYear: newManga.publicationYear || null,
|
||||
status: newManga.status || '',
|
||||
rating: newManga.rating || null,
|
||||
genres: Array.isArray(newManga.genres) ? [...newManga.genres] : [],
|
||||
alternativeSlugs: Array.isArray(newManga.alternativeSlugs) ? [...newManga.alternativeSlugs] : []
|
||||
};
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
const closeModal = () => {
|
||||
// Réinitialiser les états d'affichage
|
||||
showGenreInput.value = false;
|
||||
showAlternativeSlugInput.value = false;
|
||||
newGenre.value = '';
|
||||
newAlternativeSlug.value = '';
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const saveChanges = () => {
|
||||
// Nettoyer les données avant de les envoyer
|
||||
const dataToSave = {
|
||||
title: formData.value.title || undefined,
|
||||
description: formData.value.description || undefined,
|
||||
author: formData.value.author || undefined,
|
||||
publicationYear: formData.value.publicationYear || undefined,
|
||||
status: formData.value.status || undefined,
|
||||
rating: formData.value.rating || undefined,
|
||||
genres: formData.value.genres.length > 0 ? formData.value.genres : undefined,
|
||||
alternativeSlugs: formData.value.alternativeSlugs.length > 0 ? formData.value.alternativeSlugs : undefined
|
||||
};
|
||||
|
||||
// Supprimer les valeurs undefined
|
||||
Object.keys(dataToSave).forEach(key => {
|
||||
if (dataToSave[key] === undefined) {
|
||||
delete dataToSave[key];
|
||||
}
|
||||
});
|
||||
|
||||
emit('save', dataToSave);
|
||||
};
|
||||
|
||||
const addGenre = () => {
|
||||
if (newGenre.value.trim() && !formData.value.genres.includes(newGenre.value.trim())) {
|
||||
formData.value.genres.push(newGenre.value.trim());
|
||||
newGenre.value = '';
|
||||
showGenreInput.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeGenre = (index) => {
|
||||
formData.value.genres.splice(index, 1);
|
||||
};
|
||||
|
||||
const addAlternativeSlug = () => {
|
||||
if (newAlternativeSlug.value.trim() && !formData.value.alternativeSlugs.includes(newAlternativeSlug.value.trim())) {
|
||||
formData.value.alternativeSlugs.push(newAlternativeSlug.value.trim());
|
||||
newAlternativeSlug.value = '';
|
||||
showAlternativeSlugInput.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const removeAlternativeSlug = (index) => {
|
||||
formData.value.alternativeSlugs.splice(index, 1);
|
||||
};
|
||||
</script>
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
@@ -37,6 +37,16 @@
|
||||
@close="closePreferredSourcesModal"
|
||||
@save="savePreferredSources"
|
||||
/>
|
||||
|
||||
<!-- Modale d'édition du manga -->
|
||||
<MangaEditModal
|
||||
:is-open="isEditModalOpen"
|
||||
:manga="currentManga"
|
||||
:is-saving="isEditLoading"
|
||||
:error="editError"
|
||||
@close="closeEditModal"
|
||||
@save="saveMangaEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="isLoadingDetails" class="flex justify-center items-center h-64">
|
||||
@@ -64,9 +74,11 @@ 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 MangaEditModal from '../components/MangaEditModal.vue';
|
||||
import MangaHeader from '../components/MangaHeader.vue';
|
||||
import MangaPreferredSourcesModal from '../components/MangaPreferredSourcesModal.vue';
|
||||
import MangaVolumeList from '../components/MangaVolumeList.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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
18
src/Domain/Manga/Application/Command/EditManga.php
Normal file
18
src/Domain/Manga/Application/Command/EditManga.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\Command;
|
||||
|
||||
readonly class EditManga
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public ?string $title = null,
|
||||
public ?string $description = null,
|
||||
public ?string $author = null,
|
||||
public ?int $publicationYear = null,
|
||||
public ?array $genres = null,
|
||||
public ?string $status = null,
|
||||
public ?float $rating = null,
|
||||
public ?array $alternativeSlugs = null
|
||||
) {}
|
||||
}
|
||||
@@ -36,7 +36,9 @@ readonly class CreateMangaHandler
|
||||
$command->status,
|
||||
$command->externalId ? new ExternalId($command->externalId) : null,
|
||||
$command->imageUrl,
|
||||
$command->rating
|
||||
$command->rating,
|
||||
null, // imageUrls
|
||||
[], // alternativeSlugs
|
||||
);
|
||||
|
||||
if (!is_null($command->imageUrl)) {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Command\EditManga;
|
||||
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
|
||||
|
||||
readonly class EditMangaHandler
|
||||
{
|
||||
public function __construct(
|
||||
private MangaRepositoryInterface $mangaRepository
|
||||
) {}
|
||||
|
||||
public function handle(EditManga $command): void
|
||||
{
|
||||
$manga = $this->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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,6 +103,46 @@ 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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor\EditMangaProcessor;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\GetMangaStateProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Manga',
|
||||
operations: [
|
||||
new Put(
|
||||
uriTemplate: '/mangas/{id}/edit',
|
||||
processor: EditMangaProcessor::class,
|
||||
provider: GetMangaStateProvider::class,
|
||||
openapiContext: [
|
||||
'summary' => '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;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Domain\Manga\Application\Command\EditManga;
|
||||
use App\Domain\Manga\Application\CommandHandler\EditMangaHandler;
|
||||
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\EditMangaResource;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
readonly class EditMangaProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EditMangaHandler $handler
|
||||
) {}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
|
||||
{
|
||||
if (!$data instanceof EditMangaResource) {
|
||||
throw new \InvalidArgumentException('Invalid resource type');
|
||||
}
|
||||
|
||||
$mangaId = $uriVariables['id'] ?? null;
|
||||
if (!$mangaId) {
|
||||
throw new \InvalidArgumentException('Manga ID is required');
|
||||
}
|
||||
|
||||
$command = new EditManga(
|
||||
id: (string) $mangaId,
|
||||
title: $data->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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ readonly class GetMangaBySlugStateProvider implements ProviderInterface
|
||||
status: $response->status,
|
||||
externalId: $response->externalId,
|
||||
imageUrl: $response->imageUrl,
|
||||
thumbnailUrl: $response->thumbnailUrl,
|
||||
rating: $response->rating
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -65,7 +65,15 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
|
||||
|
||||
public function save(DomainManga $manga): void
|
||||
{
|
||||
// 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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Domain\Manga\Application\CommandHandler;
|
||||
|
||||
use App\Domain\Manga\Application\Command\EditManga;
|
||||
use App\Domain\Manga\Application\CommandHandler\EditMangaHandler;
|
||||
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||
use App\Domain\Manga\Domain\Model\Manga;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
|
||||
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
|
||||
use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EditMangaHandlerTest extends TestCase
|
||||
{
|
||||
private InMemoryMangaRepository $repository;
|
||||
private EditMangaHandler $handler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->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());
|
||||
}
|
||||
}
|
||||
174
tests/Feature/Manga/EditMangaTest.php
Normal file
174
tests/Feature/Manga/EditMangaTest.php
Normal file
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Feature\Manga;
|
||||
|
||||
use App\Tests\Feature\AbstractApiTestCase;
|
||||
use Zenstruck\Foundry\Test\ResetDatabase;
|
||||
|
||||
class EditMangaTest extends AbstractApiTestCase
|
||||
{
|
||||
use ResetDatabase;
|
||||
|
||||
public function testEditMangaSuccess(): void
|
||||
{
|
||||
// Given - Create a manga first
|
||||
$client = static::createClient();
|
||||
$response = $client->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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user