feat: commit before changing gitea
This commit is contained in:
parent
b05bd98f63
commit
ffceda606f
23
.vscode/settings.json
vendored
23
.vscode/settings.json
vendored
@@ -1,4 +1,25 @@
|
|||||||
{
|
{
|
||||||
"symfony-vscode.shellExecutable": "/bin/bash",
|
"symfony-vscode.shellExecutable": "/bin/bash",
|
||||||
"symfony-vscode.shellCommand": "docker exec mangarr-php-1 /bin/sh -c 'cd / && php \"$@\"' -- "
|
"symfony-vscode.shellCommand": "docker exec mangarr-php-1 /bin/sh -c 'cd / && php \"$@\"' -- ",
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"activityBar.activeBackground": "#2f7c47",
|
||||||
|
"activityBar.background": "#2f7c47",
|
||||||
|
"activityBar.foreground": "#e7e7e7",
|
||||||
|
"activityBar.inactiveForeground": "#e7e7e799",
|
||||||
|
"activityBarBadge.background": "#422c74",
|
||||||
|
"activityBarBadge.foreground": "#e7e7e7",
|
||||||
|
"commandCenter.border": "#e7e7e799",
|
||||||
|
"sash.hoverBorder": "#2f7c47",
|
||||||
|
"statusBar.background": "#215732",
|
||||||
|
"statusBar.foreground": "#e7e7e7",
|
||||||
|
"statusBarItem.hoverBackground": "#2f7c47",
|
||||||
|
"statusBarItem.remoteBackground": "#215732",
|
||||||
|
"statusBarItem.remoteForeground": "#e7e7e7",
|
||||||
|
"titleBar.activeBackground": "#215732",
|
||||||
|
"titleBar.activeForeground": "#e7e7e7",
|
||||||
|
"titleBar.inactiveBackground": "#21573299",
|
||||||
|
"titleBar.inactiveForeground": "#e7e7e799",
|
||||||
|
"activityBar.activeBorder": "#422c74"
|
||||||
|
},
|
||||||
|
"peacock.color": "#215732"
|
||||||
}
|
}
|
||||||
@@ -95,13 +95,35 @@ Retourne :
|
|||||||
|
|
||||||
### Import de fichier
|
### Import de fichier
|
||||||
```
|
```
|
||||||
POST /api/import/upload-file
|
POST /api/chapters/import
|
||||||
```
|
```
|
||||||
FormData :
|
FormData :
|
||||||
- `file`: Le fichier CBZ/CBR
|
- `file`: Le fichier CBZ à importer
|
||||||
- `mangaId`: ID du manga sélectionné
|
- `mangaId`: ID du manga
|
||||||
- `chapterNumber`: Numéro de chapitre (optionnel, float)
|
- `chapterNumber`: Numéro de chapitre (float, optionnel)
|
||||||
- `volumeNumber`: Numéro de volume (optionnel, float)
|
|
||||||
|
Réponse (200) :
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message": "Chapter imported successfully",
|
||||||
|
"mangaId": "uuid",
|
||||||
|
"chapterNumber": 1.5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Erreurs :
|
||||||
|
- `404`: Manga ou Chapitre non trouvé
|
||||||
|
- `422`: Paramètres invalides ou fichier absent
|
||||||
|
- `400`: Fichier CBZ invalide
|
||||||
|
|
||||||
|
### Import de volume (À venir)
|
||||||
|
```
|
||||||
|
POST /api/volumes/import
|
||||||
|
```
|
||||||
|
FormData :
|
||||||
|
- `file`: Le fichier CBZ à importer
|
||||||
|
- `mangaId`: ID du manga
|
||||||
|
- `volumeNumber`: Numéro de volume (int)
|
||||||
|
|
||||||
## Store Pinia
|
## Store Pinia
|
||||||
|
|
||||||
|
|||||||
@@ -63,26 +63,42 @@ export class ApiImportRepository {
|
|||||||
* Upload et import d'un fichier avec les informations du manga
|
* Upload et import d'un fichier avec les informations du manga
|
||||||
* @param {File} file - Fichier à uploader
|
* @param {File} file - Fichier à uploader
|
||||||
* @param {string} mangaId - ID du manga
|
* @param {string} mangaId - ID du manga
|
||||||
* @param {string|null} chapterId - ID du chapitre (optionnel)
|
* @param {number|null} chapterNumber - Numéro du chapitre (optionnel)
|
||||||
* @param {number|null} volumeNumber - Numéro du volume (optionnel)
|
* @param {number|null} volumeNumber - Numéro du volume (optionnel)
|
||||||
* @returns {Promise<Object>} - Résultat de l'import
|
* @returns {Promise<Object>} - Résultat de l'import
|
||||||
*/
|
*/
|
||||||
async importFile(file, mangaId, chapterId = null, volumeNumber = null) {
|
async importFile(file, mangaId, chapterNumber = null, volumeNumber = null) {
|
||||||
|
try {
|
||||||
|
// Déterminer s'il s'agit d'un import de chapitre ou volume
|
||||||
|
if (chapterNumber !== null && chapterNumber !== undefined) {
|
||||||
|
return await this.importChapter(file, mangaId, chapterNumber);
|
||||||
|
} else if (volumeNumber !== null && volumeNumber !== undefined) {
|
||||||
|
return await this.importVolume(file, mangaId, volumeNumber);
|
||||||
|
} else {
|
||||||
|
throw new Error('Either chapterNumber or volumeNumber must be provided');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import d'un chapitre
|
||||||
|
* @param {File} file - Fichier CBZ à uploader
|
||||||
|
* @param {string} mangaId - ID du manga
|
||||||
|
* @param {number} chapterNumber - Numéro du chapitre
|
||||||
|
* @returns {Promise<Object>} - Résultat de l'import
|
||||||
|
*/
|
||||||
|
async importChapter(file, mangaId, chapterNumber) {
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
formData.append('mangaId', mangaId);
|
formData.append('mangaId', mangaId);
|
||||||
|
formData.append('chapterNumber', chapterNumber.toString());
|
||||||
|
|
||||||
if (chapterId) {
|
console.log('Importing chapter:', chapterNumber, 'for manga:', mangaId);
|
||||||
formData.append('chapterId', chapterId);
|
const response = await fetch('/api/chapters/import', {
|
||||||
}
|
|
||||||
|
|
||||||
if (volumeNumber) {
|
|
||||||
formData.append('volumeNumber', volumeNumber.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Importing file:', file.name, 'for manga:', mangaId);
|
|
||||||
const response = await fetch('/api/import/upload-file', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
@@ -90,7 +106,60 @@ export class ApiImportRepository {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
console.error('Import failed:', response.status, errorText);
|
console.error('Import failed:', response.status, errorText);
|
||||||
throw new Error(`Failed to import file: ${response.status}`);
|
|
||||||
|
// Parse the error response if it's JSON
|
||||||
|
let errorMessage = `Failed to import chapter: ${response.status}`;
|
||||||
|
try {
|
||||||
|
const errorJson = JSON.parse(errorText);
|
||||||
|
errorMessage = errorJson.error || errorJson.details || errorMessage;
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON, use the status message
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
console.log('Import result:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API Error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import d'un volume (TODO: À implémenter)
|
||||||
|
* @param {File} file - Fichier CBZ à uploader
|
||||||
|
* @param {string} mangaId - ID du manga
|
||||||
|
* @param {number} volumeNumber - Numéro du volume
|
||||||
|
* @returns {Promise<Object>} - Résultat de l'import
|
||||||
|
*/
|
||||||
|
async importVolume(file, mangaId, volumeNumber) {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('mangaId', mangaId);
|
||||||
|
formData.append('volumeNumber', volumeNumber.toString());
|
||||||
|
|
||||||
|
console.log('Importing volume:', volumeNumber, 'for manga:', mangaId);
|
||||||
|
const response = await fetch('/api/volumes/import', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('Import failed:', response.status, errorText);
|
||||||
|
|
||||||
|
// Parse the error response if it's JSON
|
||||||
|
let errorMessage = `Failed to import volume: ${response.status}`;
|
||||||
|
try {
|
||||||
|
const errorJson = JSON.parse(errorText);
|
||||||
|
errorMessage = errorJson.error || errorJson.details || errorMessage;
|
||||||
|
} catch (e) {
|
||||||
|
// Not JSON, use the status message
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|||||||
@@ -110,3 +110,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['select-match']);
|
const emit = defineEmits(['select-match']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
src/Domain/Manga/Application/Command/ImportChapter.php
Normal file
12
src/Domain/Manga/Application/Command/ImportChapter.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Application\Command;
|
||||||
|
|
||||||
|
readonly class ImportChapter
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $mangaId,
|
||||||
|
public float $chapterNumber,
|
||||||
|
public string $fileBinary
|
||||||
|
) {}
|
||||||
|
}
|
||||||
16
src/Domain/Manga/Application/Command/ImportVolume.php
Normal file
16
src/Domain/Manga/Application/Command/ImportVolume.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Application\Command;
|
||||||
|
|
||||||
|
readonly class ImportVolume
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $mangaId,
|
||||||
|
public int $volumeNumber,
|
||||||
|
public string $fileBinary
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Application\CommandHandler;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Application\Command\ImportChapter;
|
||||||
|
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||||
|
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||||
|
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||||
|
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||||
|
use App\Domain\Manga\Domain\Model\Chapter;
|
||||||
|
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
|
||||||
|
use App\Domain\Shared\Domain\Contract\MangaPathManagerInterface;
|
||||||
|
use Ramsey\Uuid\Uuid;
|
||||||
|
|
||||||
|
readonly class ImportChapterHandler
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private MangaRepositoryInterface $mangaRepository,
|
||||||
|
private ChapterRepositoryInterface $chapterRepository,
|
||||||
|
private MangaPathManagerInterface $pathManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(ImportChapter $command): void
|
||||||
|
{
|
||||||
|
// 1. Validate that the manga exists
|
||||||
|
$manga = $this->mangaRepository->findById($command->mangaId);
|
||||||
|
if (!$manga) {
|
||||||
|
throw new MangaNotFoundException($command->mangaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate that the file is a valid CBZ
|
||||||
|
if (!$this->isValidCbzFile($command->fileBinary)) {
|
||||||
|
throw new \InvalidArgumentException('The provided file is not a valid CBZ file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check if chapter exists
|
||||||
|
$existingChapter = $this->chapterRepository->findByMangaIdAndChapterNumber(
|
||||||
|
$command->mangaId,
|
||||||
|
$command->chapterNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$existingChapter) {
|
||||||
|
throw new ChapterNotFoundException("Chapter {$command->chapterNumber} not found for manga {$command->mangaId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Save the CBZ file to storage using the path manager
|
||||||
|
$cbzPath = $this->saveCbzFile($command, $manga, $existingChapter);
|
||||||
|
|
||||||
|
// 5. Update existing chapter with new CBZ path
|
||||||
|
$updatedChapter = new Chapter(
|
||||||
|
id: new ChapterId($existingChapter->getId()),
|
||||||
|
mangaId: $existingChapter->getMangaId(),
|
||||||
|
number: $existingChapter->getNumber(),
|
||||||
|
title: $existingChapter->getTitle(),
|
||||||
|
volume: $existingChapter->getVolume(),
|
||||||
|
isVisible: $existingChapter->isVisible(),
|
||||||
|
cbzPath: $cbzPath,
|
||||||
|
createdAt: $existingChapter->getCreatedAt()
|
||||||
|
);
|
||||||
|
$this->chapterRepository->save($updatedChapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the binary data is a valid CBZ (ZIP) file
|
||||||
|
*/
|
||||||
|
private function isValidCbzFile(string $fileBinary): bool
|
||||||
|
{
|
||||||
|
// CBZ files are ZIP archives, check for ZIP magic number
|
||||||
|
$zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04
|
||||||
|
|
||||||
|
return strpos($fileBinary, $zipMagicNumber) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the CBZ file to storage and return the path
|
||||||
|
*/
|
||||||
|
private function saveCbzFile(ImportChapter $command, \App\Domain\Manga\Domain\Model\Manga $manga, Chapter $chapter): string
|
||||||
|
{
|
||||||
|
// Build the final CBZ path using the path manager (creates directories)
|
||||||
|
$volumeNumber = $chapter->getVolume() ?? 0;
|
||||||
|
$cbzPath = $this->pathManager->buildChapterCbzPath(
|
||||||
|
$manga->getTitle()->getValue(),
|
||||||
|
(string)$manga->getPublicationYear(),
|
||||||
|
$volumeNumber,
|
||||||
|
(string)$command->chapterNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the binary content directly to the CBZ path
|
||||||
|
if (!file_put_contents($cbzPath, $command->fileBinary)) {
|
||||||
|
throw new \RuntimeException('Failed to save CBZ file');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cbzPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Application\CommandHandler;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Application\Command\ImportVolume;
|
||||||
|
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||||
|
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
|
||||||
|
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||||
|
use App\Domain\Manga\Domain\Model\Chapter;
|
||||||
|
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
|
||||||
|
use App\Domain\Shared\Domain\Contract\MangaPathManagerInterface;
|
||||||
|
|
||||||
|
readonly class ImportVolumeHandler
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private MangaRepositoryInterface $mangaRepository,
|
||||||
|
private ChapterRepositoryInterface $chapterRepository,
|
||||||
|
private MangaPathManagerInterface $pathManager
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function handle(ImportVolume $command): void
|
||||||
|
{
|
||||||
|
// 1. Validate that the manga exists
|
||||||
|
$manga = $this->mangaRepository->findById($command->mangaId);
|
||||||
|
if (!$manga) {
|
||||||
|
throw new MangaNotFoundException($command->mangaId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Validate that the file is a valid CBZ
|
||||||
|
if (!$this->isValidCbzFile($command->fileBinary)) {
|
||||||
|
throw new \InvalidArgumentException('The provided file is not a valid CBZ file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Get all chapters for this volume
|
||||||
|
$chapters = $this->chapterRepository->findByMangaIdAndVolume(
|
||||||
|
$command->mangaId,
|
||||||
|
$command->volumeNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
if (empty($chapters)) {
|
||||||
|
throw new \InvalidArgumentException(
|
||||||
|
"No chapters found for manga {$command->mangaId} in volume {$command->volumeNumber}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Save the CBZ file to storage using the path manager
|
||||||
|
$cbzPath = $this->saveCbzFile($command, $manga);
|
||||||
|
|
||||||
|
// 5. Update all chapters with the volume CBZ path
|
||||||
|
foreach ($chapters as $chapter) {
|
||||||
|
$updatedChapter = new Chapter(
|
||||||
|
id: new ChapterId($chapter->getId()),
|
||||||
|
mangaId: $chapter->getMangaId(),
|
||||||
|
number: $chapter->getNumber(),
|
||||||
|
title: $chapter->getTitle(),
|
||||||
|
volume: $chapter->getVolume(),
|
||||||
|
isVisible: $chapter->isVisible(),
|
||||||
|
cbzPath: $cbzPath,
|
||||||
|
createdAt: $chapter->getCreatedAt()
|
||||||
|
);
|
||||||
|
$this->chapterRepository->save($updatedChapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the binary data is a valid CBZ (ZIP) file
|
||||||
|
*/
|
||||||
|
private function isValidCbzFile(string $fileBinary): bool
|
||||||
|
{
|
||||||
|
// CBZ files are ZIP archives, check for ZIP magic number
|
||||||
|
$zipMagicNumber = "\x50\x4b\x03\x04"; // PK\x03\x04
|
||||||
|
|
||||||
|
return strpos($fileBinary, $zipMagicNumber) === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the CBZ file to storage and return the path
|
||||||
|
*/
|
||||||
|
private function saveCbzFile(ImportVolume $command, \App\Domain\Manga\Domain\Model\Manga $manga): string
|
||||||
|
{
|
||||||
|
// Build the final CBZ path using the path manager (creates directories)
|
||||||
|
$cbzPath = $this->pathManager->buildVolumeCbzPath(
|
||||||
|
$manga->getTitle()->getValue(),
|
||||||
|
(string)$manga->getPublicationYear(),
|
||||||
|
$command->volumeNumber
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the binary content directly to the CBZ path
|
||||||
|
if (!file_put_contents($cbzPath, $command->fileBinary)) {
|
||||||
|
throw new \RuntimeException('Failed to save CBZ file');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cbzPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ readonly class DownloadVolumeHandler implements QueryHandlerInterface
|
|||||||
$cbzPaths[] = $chapter->getCbzPath();
|
$cbzPaths[] = $chapter->getCbzPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
$volumeName = sprintf('%s-volume-%d',
|
$volumeName = sprintf('%s_vol%d',
|
||||||
$manga->getSlug()->getValue(),
|
$manga->getSlug()->getValue(),
|
||||||
$query->volume
|
$query->volume
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface ChapterRepositoryInterface
|
|||||||
{
|
{
|
||||||
public function findById(string $id): ?Chapter;
|
public function findById(string $id): ?Chapter;
|
||||||
public function findVisibleById(string $id): ?Chapter;
|
public function findVisibleById(string $id): ?Chapter;
|
||||||
|
public function findByMangaIdAndChapterNumber(string $mangaId, float $chapterNumber): ?Chapter;
|
||||||
public function save(Chapter $chapter): void;
|
public function save(Chapter $chapter): void;
|
||||||
public function delete(Chapter $chapter): void;
|
public function delete(Chapter $chapter): void;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Controller;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Application\Command\ImportChapter;
|
||||||
|
use App\Domain\Manga\Application\CommandHandler\ImportChapterHandler;
|
||||||
|
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||||
|
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||||
|
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\ImportChapterResource;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
|
||||||
|
#[AsController]
|
||||||
|
final class ImportChapterController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ImportChapterHandler $commandHandler
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(Request $request): Response
|
||||||
|
{
|
||||||
|
// Get form parameters
|
||||||
|
$mangaId = $request->request->get('mangaId');
|
||||||
|
$chapterNumber = $request->request->get('chapterNumber');
|
||||||
|
$uploadedFile = $request->files->get('file');
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!$mangaId) {
|
||||||
|
return $this->json([
|
||||||
|
['propertyPath' => 'mangaId', 'message' => 'mangaId is required']
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$chapterNumber) {
|
||||||
|
return $this->json([
|
||||||
|
['propertyPath' => 'chapterNumber', 'message' => 'chapterNumber is required']
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$uploadedFile) {
|
||||||
|
return $this->json([
|
||||||
|
['propertyPath' => 'file', 'message' => 'Please upload a file']
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file
|
||||||
|
$errors = $this->validateFile($uploadedFile);
|
||||||
|
if (!empty($errors)) {
|
||||||
|
return $this->json($errors, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read file binary content
|
||||||
|
$fileBinary = file_get_contents($uploadedFile->getPathname());
|
||||||
|
if ($fileBinary === false) {
|
||||||
|
return $this->json([
|
||||||
|
['propertyPath' => 'file', 'message' => 'Failed to read the uploaded file']
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the command
|
||||||
|
$command = new ImportChapter(
|
||||||
|
mangaId: $mangaId,
|
||||||
|
chapterNumber: (float) $chapterNumber,
|
||||||
|
fileBinary: $fileBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute the import
|
||||||
|
$this->commandHandler->handle($command);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'message' => 'Chapter imported successfully',
|
||||||
|
'mangaId' => $mangaId,
|
||||||
|
'chapterNumber' => $chapterNumber
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (MangaNotFoundException $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => 'Manga not found',
|
||||||
|
'details' => $e->getMessage()
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (ChapterNotFoundException $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => 'Chapter not found',
|
||||||
|
'details' => $e->getMessage()
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => 'Invalid file',
|
||||||
|
'details' => $e->getMessage()
|
||||||
|
], 400);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => 'Import failed',
|
||||||
|
'details' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateFile($uploadedFile): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Check if file is valid
|
||||||
|
if (!$uploadedFile->isValid()) {
|
||||||
|
$errors[] = [
|
||||||
|
'propertyPath' => 'file',
|
||||||
|
'message' => 'The uploaded file is not valid: ' . $uploadedFile->getErrorMessage()
|
||||||
|
];
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (500MB max)
|
||||||
|
$maxSize = 500 * 1024 * 1024; // 500MB in bytes
|
||||||
|
if ($uploadedFile->getSize() > $maxSize) {
|
||||||
|
$errors[] = [
|
||||||
|
'propertyPath' => 'file',
|
||||||
|
'message' => 'The uploaded file is too large. Allowed size is 500MB.'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file extension
|
||||||
|
$allowedExtensions = ['cbz'];
|
||||||
|
$extension = strtolower($uploadedFile->getClientOriginalExtension());
|
||||||
|
|
||||||
|
if (!in_array($extension, $allowedExtensions)) {
|
||||||
|
$errors[] = [
|
||||||
|
'propertyPath' => 'file',
|
||||||
|
'message' => 'Please upload a valid CBZ file'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Controller;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Application\Command\ImportVolume;
|
||||||
|
use App\Domain\Manga\Application\CommandHandler\ImportVolumeHandler;
|
||||||
|
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||||
|
use App\Domain\Manga\Infrastructure\ApiPlatform\Resource\ImportVolumeResource;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\AsController;
|
||||||
|
|
||||||
|
#[AsController]
|
||||||
|
final class ImportVolumeController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ImportVolumeHandler $commandHandler
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(Request $request): Response
|
||||||
|
{
|
||||||
|
// Get form parameters
|
||||||
|
$mangaId = $request->request->get('mangaId');
|
||||||
|
$volumeNumber = $request->request->get('volumeNumber');
|
||||||
|
$uploadedFile = $request->files->get('file');
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!$mangaId) {
|
||||||
|
return $this->json([
|
||||||
|
['propertyPath' => 'mangaId', 'message' => 'mangaId is required']
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$volumeNumber) {
|
||||||
|
return $this->json([
|
||||||
|
['propertyPath' => 'volumeNumber', 'message' => 'volumeNumber is required']
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$uploadedFile) {
|
||||||
|
return $this->json([
|
||||||
|
['propertyPath' => 'file', 'message' => 'Please upload a file']
|
||||||
|
], 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file
|
||||||
|
$errors = $this->validateFile($uploadedFile);
|
||||||
|
if (!empty($errors)) {
|
||||||
|
return $this->json($errors, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Read file binary content
|
||||||
|
$fileBinary = file_get_contents($uploadedFile->getPathname());
|
||||||
|
if ($fileBinary === false) {
|
||||||
|
return $this->json([
|
||||||
|
['propertyPath' => 'file', 'message' => 'Failed to read the uploaded file']
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the command
|
||||||
|
$command = new ImportVolume(
|
||||||
|
mangaId: $mangaId,
|
||||||
|
volumeNumber: (int) $volumeNumber,
|
||||||
|
fileBinary: $fileBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute the import
|
||||||
|
$this->commandHandler->handle($command);
|
||||||
|
|
||||||
|
return $this->json([
|
||||||
|
'message' => 'Volume imported successfully',
|
||||||
|
'mangaId' => $mangaId,
|
||||||
|
'volumeNumber' => (int) $volumeNumber
|
||||||
|
], 200);
|
||||||
|
|
||||||
|
} catch (MangaNotFoundException $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => 'Manga not found',
|
||||||
|
'details' => $e->getMessage()
|
||||||
|
], 404);
|
||||||
|
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
$statusCode = str_contains($e->getMessage(), 'not found') ? 404 : 400;
|
||||||
|
return $this->json([
|
||||||
|
'error' => 'Invalid request',
|
||||||
|
'details' => $e->getMessage()
|
||||||
|
], $statusCode);
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return $this->json([
|
||||||
|
'error' => 'Import failed',
|
||||||
|
'details' => $e->getMessage()
|
||||||
|
], 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function validateFile($uploadedFile): array
|
||||||
|
{
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
// Check if file is valid
|
||||||
|
if (!$uploadedFile->isValid()) {
|
||||||
|
$errors[] = [
|
||||||
|
'propertyPath' => 'file',
|
||||||
|
'message' => 'The uploaded file is not valid: ' . $uploadedFile->getErrorMessage()
|
||||||
|
];
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (500MB max)
|
||||||
|
$maxSize = 500 * 1024 * 1024; // 500MB in bytes
|
||||||
|
if ($uploadedFile->getSize() > $maxSize) {
|
||||||
|
$errors[] = [
|
||||||
|
'propertyPath' => 'file',
|
||||||
|
'message' => 'The uploaded file is too large. Allowed size is 500MB.'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file extension
|
||||||
|
$allowedExtensions = ['cbz'];
|
||||||
|
$extension = strtolower($uploadedFile->getClientOriginalExtension());
|
||||||
|
|
||||||
|
if (!in_array($extension, $allowedExtensions)) {
|
||||||
|
$errors[] = [
|
||||||
|
'propertyPath' => 'file',
|
||||||
|
'message' => 'Please upload a valid CBZ file'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use App\Domain\Manga\Infrastructure\ApiPlatform\Controller\ImportChapterController;
|
||||||
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
shortName: 'ImportChapter',
|
||||||
|
operations: [
|
||||||
|
new Post(
|
||||||
|
uriTemplate: '/chapters/import',
|
||||||
|
controller: ImportChapterController::class,
|
||||||
|
deserialize: false,
|
||||||
|
openapiContext: [
|
||||||
|
'summary' => 'Import a chapter from CBZ file',
|
||||||
|
'description' => 'Imports a CBZ file for an existing chapter and stores it',
|
||||||
|
'requestBody' => [
|
||||||
|
'content' => [
|
||||||
|
'multipart/form-data' => [
|
||||||
|
'schema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'required' => ['mangaId', 'chapterNumber', 'file'],
|
||||||
|
'properties' => [
|
||||||
|
'mangaId' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'format' => 'uuid',
|
||||||
|
'description' => 'The manga UUID'
|
||||||
|
],
|
||||||
|
'chapterNumber' => [
|
||||||
|
'type' => 'number',
|
||||||
|
'description' => 'The chapter number (e.g., 1.5)'
|
||||||
|
],
|
||||||
|
'file' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'format' => 'binary',
|
||||||
|
'description' => 'CBZ file to import (max 500MB)'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'responses' => [
|
||||||
|
'200' => [
|
||||||
|
'description' => 'Chapter imported successfully',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'message' => ['type' => 'string'],
|
||||||
|
'chapterId' => ['type' => 'string']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
class ImportChapterResource
|
||||||
|
{
|
||||||
|
public ?string $mangaId = null;
|
||||||
|
|
||||||
|
public ?float $chapterNumber = null;
|
||||||
|
|
||||||
|
public ?File $file = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use App\Domain\Manga\Infrastructure\ApiPlatform\Controller\ImportVolumeController;
|
||||||
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
shortName: 'ImportVolume',
|
||||||
|
operations: [
|
||||||
|
new Post(
|
||||||
|
uriTemplate: '/volumes/import',
|
||||||
|
controller: ImportVolumeController::class,
|
||||||
|
deserialize: false,
|
||||||
|
openapiContext: [
|
||||||
|
'summary' => 'Import a volume from CBZ file',
|
||||||
|
'description' => 'Imports a CBZ file for an existing volume and updates all chapters',
|
||||||
|
'requestBody' => [
|
||||||
|
'content' => [
|
||||||
|
'multipart/form-data' => [
|
||||||
|
'schema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'required' => ['mangaId', 'volumeNumber', 'file'],
|
||||||
|
'properties' => [
|
||||||
|
'mangaId' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'format' => 'uuid',
|
||||||
|
'description' => 'The manga UUID'
|
||||||
|
],
|
||||||
|
'volumeNumber' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'description' => 'The volume number'
|
||||||
|
],
|
||||||
|
'file' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'format' => 'binary',
|
||||||
|
'description' => 'CBZ file to import (max 500MB)'
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'responses' => [
|
||||||
|
'200' => [
|
||||||
|
'description' => 'Volume imported successfully',
|
||||||
|
'content' => [
|
||||||
|
'application/json' => [
|
||||||
|
'schema' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'message' => ['type' => 'string'],
|
||||||
|
'mangaId' => ['type' => 'string'],
|
||||||
|
'volumeNumber' => ['type' => 'integer']
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
class ImportVolumeResource
|
||||||
|
{
|
||||||
|
public ?string $mangaId = null;
|
||||||
|
|
||||||
|
public ?int $volumeNumber = null;
|
||||||
|
|
||||||
|
public ?File $file = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -36,6 +36,21 @@ readonly class LegacyChapterRepository implements ChapterRepositoryInterface
|
|||||||
return $entity ? $this->toDomainModel($entity) : null;
|
return $entity ? $this->toDomainModel($entity) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function findByMangaIdAndChapterNumber(string $mangaId, float $chapterNumber): ?Chapter
|
||||||
|
{
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()
|
||||||
|
->select('c')
|
||||||
|
->from(ChapterEntity::class, 'c')
|
||||||
|
->where('c.manga = :mangaId')
|
||||||
|
->andWhere('c.number = :chapterNumber')
|
||||||
|
->setParameter('mangaId', $mangaId)
|
||||||
|
->setParameter('chapterNumber', $chapterNumber);
|
||||||
|
|
||||||
|
$entity = $qb->getQuery()->getOneOrNullResult();
|
||||||
|
|
||||||
|
return $entity ? $this->toDomainModel($entity) : null;
|
||||||
|
}
|
||||||
|
|
||||||
public function save(Chapter $chapter): void
|
public function save(Chapter $chapter): void
|
||||||
{
|
{
|
||||||
$entity = $this->entityManager->find(ChapterEntity::class, $chapter->getId());
|
$entity = $this->entityManager->find(ChapterEntity::class, $chapter->getId());
|
||||||
|
|||||||
95
tests/Domain/Manga/Adapter/InMemoryChapterRepository.php
Normal file
95
tests/Domain/Manga/Adapter/InMemoryChapterRepository.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Domain\Manga\Adapter;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Domain\Contract\Repository\ChapterRepositoryInterface;
|
||||||
|
use App\Domain\Manga\Domain\Model\Chapter;
|
||||||
|
|
||||||
|
class InMemoryChapterRepository implements ChapterRepositoryInterface
|
||||||
|
{
|
||||||
|
/** @var array<string, Chapter> */
|
||||||
|
private array $chapters = [];
|
||||||
|
|
||||||
|
public function findById(string $id): ?Chapter
|
||||||
|
{
|
||||||
|
return $this->chapters[$id] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findVisibleById(string $id): ?Chapter
|
||||||
|
{
|
||||||
|
$chapter = $this->chapters[$id] ?? null;
|
||||||
|
if ($chapter && $chapter->isVisible()) {
|
||||||
|
return $chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByMangaIdAndChapterNumber(string $mangaId, float $chapterNumber): ?Chapter
|
||||||
|
{
|
||||||
|
foreach ($this->chapters as $chapter) {
|
||||||
|
if ($chapter->getMangaId() === $mangaId && $chapter->getNumber() === $chapterNumber) {
|
||||||
|
return $chapter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(Chapter $chapter): void
|
||||||
|
{
|
||||||
|
$this->chapters[$chapter->getId()] = $chapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(Chapter $chapter): void
|
||||||
|
{
|
||||||
|
unset($this->chapters[$chapter->getId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByMangaIdAndVolume(string $mangaId, int $volume): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
$this->chapters,
|
||||||
|
fn (Chapter $chapter) => $chapter->getMangaId() === $mangaId && $chapter->getVolume() === $volume
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findVisibleByMangaIdAndVolume(string $mangaId, int $volume): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
$this->chapters,
|
||||||
|
fn (Chapter $chapter) =>
|
||||||
|
$chapter->getMangaId() === $mangaId &&
|
||||||
|
$chapter->getVolume() === $volume &&
|
||||||
|
$chapter->isVisible()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findVisibleWithCbzByMangaIdAndVolume(string $mangaId, int $volume): array
|
||||||
|
{
|
||||||
|
return array_filter(
|
||||||
|
$this->chapters,
|
||||||
|
fn (Chapter $chapter) =>
|
||||||
|
$chapter->getMangaId() === $mangaId &&
|
||||||
|
$chapter->getVolume() === $volume &&
|
||||||
|
$chapter->isVisible() &&
|
||||||
|
$chapter->isAvailable()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all chapters
|
||||||
|
*/
|
||||||
|
public function getAll(): array
|
||||||
|
{
|
||||||
|
return array_values($this->chapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all chapters
|
||||||
|
*/
|
||||||
|
public function clear(): void
|
||||||
|
{
|
||||||
|
$this->chapters = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
91
tests/Domain/Manga/Adapter/InMemoryPathManager.php
Normal file
91
tests/Domain/Manga/Adapter/InMemoryPathManager.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Domain\Manga\Adapter;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Domain\Contract\MangaPathManagerInterface;
|
||||||
|
|
||||||
|
class InMemoryPathManager implements MangaPathManagerInterface
|
||||||
|
{
|
||||||
|
/** @var array<string, string> */
|
||||||
|
private array $files = [];
|
||||||
|
|
||||||
|
public function getMangaDirectory(string $mangaTitle, string $publicationYear): string
|
||||||
|
{
|
||||||
|
$dir = '/tmp/manga/' . $this->slugify($mangaTitle) . '_' . $publicationYear;
|
||||||
|
$this->ensureDirectory($dir);
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVolumeDirectory(string $mangaTitle, string $publicationYear, int $volumeNumber): string
|
||||||
|
{
|
||||||
|
$dir = $this->getMangaDirectory($mangaTitle, $publicationYear) . '/volume_' . $volumeNumber;
|
||||||
|
$this->ensureDirectory($dir);
|
||||||
|
return $dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildChapterCbzPath(string $mangaTitle, string $publicationYear, int $volumeNumber, string $chapterNumber): string
|
||||||
|
{
|
||||||
|
$dir = $this->getVolumeDirectory($mangaTitle, $publicationYear, $volumeNumber);
|
||||||
|
return $dir . '/' . $this->slugify($mangaTitle) . '_vol' . $volumeNumber . '_ch' . $chapterNumber . '.cbz';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildVolumeCbzPath(string $mangaTitle, string $publicationYear, int $volumeNumber): string
|
||||||
|
{
|
||||||
|
return $this->getVolumeDirectory($mangaTitle, $publicationYear, $volumeNumber)
|
||||||
|
. '/' . $this->slugify($mangaTitle) . '_vol' . $volumeNumber . '.cbz';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createCbzArchive(array $files, string $cbzPath): void
|
||||||
|
{
|
||||||
|
// For testing, just store the CBZ path
|
||||||
|
$this->files[$cbzPath] = json_encode($files);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function moveFileTo(string $sourcePath, string $destinationPath): void
|
||||||
|
{
|
||||||
|
// In-memory: just copy content if source exists
|
||||||
|
if (file_exists($sourcePath)) {
|
||||||
|
$content = file_get_contents($sourcePath);
|
||||||
|
$this->files[$destinationPath] = $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fileExists(string $path): bool
|
||||||
|
{
|
||||||
|
return isset($this->files[$path]) || file_exists($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all stored files
|
||||||
|
*/
|
||||||
|
public function getFiles(): array
|
||||||
|
{
|
||||||
|
return $this->files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all stored files
|
||||||
|
*/
|
||||||
|
public function clear(): void
|
||||||
|
{
|
||||||
|
$this->files = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function slugify(string $text): string
|
||||||
|
{
|
||||||
|
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
|
||||||
|
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
|
||||||
|
$text = preg_replace('~[^-\w]+~', '', $text);
|
||||||
|
$text = trim($text, '-');
|
||||||
|
$text = preg_replace('~-+~', '-', $text);
|
||||||
|
$text = strtolower($text);
|
||||||
|
return $text ?: 'n-a';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureDirectory(string $path): void
|
||||||
|
{
|
||||||
|
if (!is_dir($path)) {
|
||||||
|
mkdir($path, 0777, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Domain\Manga\Application\CommandHandler;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Application\Command\ImportChapter;
|
||||||
|
use App\Domain\Manga\Application\CommandHandler\ImportChapterHandler;
|
||||||
|
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||||
|
use App\Domain\Manga\Domain\Exception\ChapterNotFoundException;
|
||||||
|
use App\Domain\Manga\Domain\Model\Chapter;
|
||||||
|
use App\Domain\Manga\Domain\Model\Manga;
|
||||||
|
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
|
||||||
|
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\InMemoryChapterRepository;
|
||||||
|
use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository;
|
||||||
|
use App\Tests\Domain\Manga\Adapter\InMemoryPathManager;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ImportChapterHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
private InMemoryMangaRepository $mangaRepository;
|
||||||
|
private InMemoryChapterRepository $chapterRepository;
|
||||||
|
private InMemoryPathManager $pathManager;
|
||||||
|
private ImportChapterHandler $handler;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->mangaRepository = new InMemoryMangaRepository();
|
||||||
|
$this->chapterRepository = new InMemoryChapterRepository();
|
||||||
|
$this->pathManager = new InMemoryPathManager();
|
||||||
|
$this->handler = new ImportChapterHandler(
|
||||||
|
$this->mangaRepository,
|
||||||
|
$this->chapterRepository,
|
||||||
|
$this->pathManager
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_throws_exception_when_chapter_not_found(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$mangaId = 'manga-123';
|
||||||
|
$manga = new Manga(
|
||||||
|
new MangaId($mangaId),
|
||||||
|
new MangaTitle('One Piece'),
|
||||||
|
new MangaSlug('one-piece'),
|
||||||
|
'Description',
|
||||||
|
'Eiichiro Oda',
|
||||||
|
1997,
|
||||||
|
['action', 'adventure'],
|
||||||
|
'ongoing'
|
||||||
|
);
|
||||||
|
$this->mangaRepository->save($manga);
|
||||||
|
|
||||||
|
$cbzBinary = $this->createValidCbzBinary();
|
||||||
|
$command = new ImportChapter(
|
||||||
|
mangaId: $mangaId,
|
||||||
|
chapterNumber: 1.5,
|
||||||
|
fileBinary: $cbzBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->expectException(ChapterNotFoundException::class);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$this->handler->handle($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_updates_existing_chapter_with_new_cbz(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$mangaId = 'manga-123';
|
||||||
|
$manga = new Manga(
|
||||||
|
new MangaId($mangaId),
|
||||||
|
new MangaTitle('One Piece'),
|
||||||
|
new MangaSlug('one-piece'),
|
||||||
|
'Description',
|
||||||
|
'Eiichiro Oda',
|
||||||
|
1997,
|
||||||
|
['action', 'adventure'],
|
||||||
|
'ongoing'
|
||||||
|
);
|
||||||
|
$this->mangaRepository->save($manga);
|
||||||
|
|
||||||
|
// Create an existing chapter without CBZ
|
||||||
|
$existingChapter = new Chapter(
|
||||||
|
new ChapterId('chapter-123'),
|
||||||
|
$mangaId,
|
||||||
|
1.5,
|
||||||
|
'Chapter 1.5',
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
$this->chapterRepository->save($existingChapter);
|
||||||
|
|
||||||
|
// Import the same chapter with CBZ
|
||||||
|
$cbzBinary = $this->createValidCbzBinary();
|
||||||
|
$command = new ImportChapter(
|
||||||
|
mangaId: $mangaId,
|
||||||
|
chapterNumber: 1.5,
|
||||||
|
fileBinary: $cbzBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$this->handler->handle($command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$chapters = $this->chapterRepository->getAll();
|
||||||
|
$this->assertCount(1, $chapters); // Still only one chapter
|
||||||
|
|
||||||
|
$updatedChapter = $chapters[0];
|
||||||
|
$this->assertEquals('chapter-123', $updatedChapter->getId());
|
||||||
|
$this->assertEquals($mangaId, $updatedChapter->getMangaId());
|
||||||
|
$this->assertEquals(1.5, $updatedChapter->getNumber());
|
||||||
|
$this->assertEquals('Chapter 1.5', $updatedChapter->getTitle()); // Title preserved
|
||||||
|
$this->assertEquals(1, $updatedChapter->getVolume()); // Volume preserved
|
||||||
|
$this->assertTrue($updatedChapter->isVisible());
|
||||||
|
$this->assertTrue($updatedChapter->isAvailable()); // Now has CBZ
|
||||||
|
$this->assertStringContainsString('_vol1_ch1.5.cbz', $updatedChapter->getCbzPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_throws_exception_when_manga_not_found(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$cbzBinary = $this->createValidCbzBinary();
|
||||||
|
$command = new ImportChapter(
|
||||||
|
mangaId: 'non-existent-manga',
|
||||||
|
chapterNumber: 1.0,
|
||||||
|
fileBinary: $cbzBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->expectException(MangaNotFoundException::class);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$this->handler->handle($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_throws_exception_when_file_is_not_valid_cbz(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$mangaId = 'manga-123';
|
||||||
|
$manga = new Manga(
|
||||||
|
new MangaId($mangaId),
|
||||||
|
new MangaTitle('One Piece'),
|
||||||
|
new MangaSlug('one-piece'),
|
||||||
|
'Description',
|
||||||
|
'Eiichiro Oda',
|
||||||
|
1997,
|
||||||
|
['action', 'adventure'],
|
||||||
|
'ongoing'
|
||||||
|
);
|
||||||
|
$this->mangaRepository->save($manga);
|
||||||
|
|
||||||
|
$invalidBinary = 'This is not a CBZ file';
|
||||||
|
$command = new ImportChapter(
|
||||||
|
mangaId: $mangaId,
|
||||||
|
chapterNumber: 1.0,
|
||||||
|
fileBinary: $invalidBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('The provided file is not a valid CBZ file');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$this->handler->handle($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a minimal valid CBZ (ZIP) binary for testing
|
||||||
|
*/
|
||||||
|
private function createValidCbzBinary(): string
|
||||||
|
{
|
||||||
|
$tmpFile = tempnam(sys_get_temp_dir(), 'cbz');
|
||||||
|
|
||||||
|
// Delete the empty file created by tempnam
|
||||||
|
unlink($tmpFile);
|
||||||
|
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
|
||||||
|
// Create a new ZIP archive (avoid opening empty file)
|
||||||
|
if ($zip->open($tmpFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||||
|
throw new \RuntimeException('Cannot create test CBZ file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a dummy image file to the ZIP
|
||||||
|
$zip->addFromString('image1.jpg', 'fake-image-data');
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
$binaryContent = file_get_contents($tmpFile);
|
||||||
|
unlink($tmpFile);
|
||||||
|
|
||||||
|
return $binaryContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Domain\Manga\Application\CommandHandler;
|
||||||
|
|
||||||
|
use App\Domain\Manga\Application\Command\ImportVolume;
|
||||||
|
use App\Domain\Manga\Application\CommandHandler\ImportVolumeHandler;
|
||||||
|
use App\Domain\Manga\Domain\Exception\MangaNotFoundException;
|
||||||
|
use App\Domain\Manga\Domain\Model\Chapter;
|
||||||
|
use App\Domain\Manga\Domain\Model\Manga;
|
||||||
|
use App\Domain\Manga\Domain\Model\ValueObject\ChapterId;
|
||||||
|
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\InMemoryChapterRepository;
|
||||||
|
use App\Tests\Domain\Manga\Adapter\InMemoryMangaRepository;
|
||||||
|
use App\Tests\Domain\Manga\Adapter\InMemoryPathManager;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class ImportVolumeHandlerTest extends TestCase
|
||||||
|
{
|
||||||
|
private InMemoryMangaRepository $mangaRepository;
|
||||||
|
private InMemoryChapterRepository $chapterRepository;
|
||||||
|
private InMemoryPathManager $pathManager;
|
||||||
|
private ImportVolumeHandler $handler;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->mangaRepository = new InMemoryMangaRepository();
|
||||||
|
$this->chapterRepository = new InMemoryChapterRepository();
|
||||||
|
$this->pathManager = new InMemoryPathManager();
|
||||||
|
$this->handler = new ImportVolumeHandler(
|
||||||
|
$this->mangaRepository,
|
||||||
|
$this->chapterRepository,
|
||||||
|
$this->pathManager
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_updates_all_chapters_in_volume(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$mangaId = 'manga-123';
|
||||||
|
$volumeNumber = 1;
|
||||||
|
$manga = new Manga(
|
||||||
|
new MangaId($mangaId),
|
||||||
|
new MangaTitle('One Piece'),
|
||||||
|
new MangaSlug('one-piece'),
|
||||||
|
'Description',
|
||||||
|
'Eiichiro Oda',
|
||||||
|
1997,
|
||||||
|
['action', 'adventure'],
|
||||||
|
'ongoing'
|
||||||
|
);
|
||||||
|
$this->mangaRepository->save($manga);
|
||||||
|
|
||||||
|
// Create chapters in volume 1
|
||||||
|
for ($i = 1; $i <= 3; $i++) {
|
||||||
|
$chapter = new Chapter(
|
||||||
|
new ChapterId("chapter-$i"),
|
||||||
|
$mangaId,
|
||||||
|
(float)$i,
|
||||||
|
"Chapter $i",
|
||||||
|
$volumeNumber,
|
||||||
|
true,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
$this->chapterRepository->save($chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cbzBinary = $this->createValidCbzBinary();
|
||||||
|
$command = new ImportVolume(
|
||||||
|
mangaId: $mangaId,
|
||||||
|
volumeNumber: $volumeNumber,
|
||||||
|
fileBinary: $cbzBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$this->handler->handle($command);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$chapters = $this->chapterRepository->findByMangaIdAndVolume($mangaId, $volumeNumber);
|
||||||
|
$this->assertCount(3, $chapters);
|
||||||
|
|
||||||
|
foreach ($chapters as $chapter) {
|
||||||
|
$this->assertTrue($chapter->isAvailable());
|
||||||
|
$this->assertStringContainsString('_vol' . $volumeNumber . '.cbz', $chapter->getCbzPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_throws_exception_when_manga_not_found(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$cbzBinary = $this->createValidCbzBinary();
|
||||||
|
$command = new ImportVolume(
|
||||||
|
mangaId: 'non-existent-manga',
|
||||||
|
volumeNumber: 1,
|
||||||
|
fileBinary: $cbzBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->expectException(MangaNotFoundException::class);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$this->handler->handle($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_throws_exception_when_file_is_not_valid_cbz(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$mangaId = 'manga-123';
|
||||||
|
$manga = new Manga(
|
||||||
|
new MangaId($mangaId),
|
||||||
|
new MangaTitle('One Piece'),
|
||||||
|
new MangaSlug('one-piece'),
|
||||||
|
'Description',
|
||||||
|
'Eiichiro Oda',
|
||||||
|
1997,
|
||||||
|
['action', 'adventure'],
|
||||||
|
'ongoing'
|
||||||
|
);
|
||||||
|
$this->mangaRepository->save($manga);
|
||||||
|
|
||||||
|
$invalidBinary = 'This is not a CBZ file';
|
||||||
|
$command = new ImportVolume(
|
||||||
|
mangaId: $mangaId,
|
||||||
|
volumeNumber: 1,
|
||||||
|
fileBinary: $invalidBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('The provided file is not a valid CBZ file');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$this->handler->handle($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_throws_exception_when_no_chapters_in_volume(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$mangaId = 'manga-123';
|
||||||
|
$manga = new Manga(
|
||||||
|
new MangaId($mangaId),
|
||||||
|
new MangaTitle('One Piece'),
|
||||||
|
new MangaSlug('one-piece'),
|
||||||
|
'Description',
|
||||||
|
'Eiichiro Oda',
|
||||||
|
1997,
|
||||||
|
['action', 'adventure'],
|
||||||
|
'ongoing'
|
||||||
|
);
|
||||||
|
$this->mangaRepository->save($manga);
|
||||||
|
|
||||||
|
$cbzBinary = $this->createValidCbzBinary();
|
||||||
|
$command = new ImportVolume(
|
||||||
|
mangaId: $mangaId,
|
||||||
|
volumeNumber: 999, // Non-existent volume
|
||||||
|
fileBinary: $cbzBinary
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
|
$this->expectExceptionMessage('No chapters found');
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$this->handler->handle($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a minimal valid CBZ (ZIP) binary for testing
|
||||||
|
*/
|
||||||
|
private function createValidCbzBinary(): string
|
||||||
|
{
|
||||||
|
$tmpFile = tempnam(sys_get_temp_dir(), 'cbz_');
|
||||||
|
unlink($tmpFile);
|
||||||
|
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
if ($zip->open($tmpFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||||
|
throw new \RuntimeException('Cannot create test CBZ file');
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->addFromString('image1.jpg', 'fake-image-data');
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
$binaryContent = file_get_contents($tmpFile);
|
||||||
|
unlink($tmpFile);
|
||||||
|
|
||||||
|
return $binaryContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class DownloadVolumeTest extends AbstractApiTestCase
|
|||||||
$this->assertResponseHeaderSame('Content-Type', 'application/x-cbz');
|
$this->assertResponseHeaderSame('Content-Type', 'application/x-cbz');
|
||||||
$contentDisposition = static::getClient()->getResponse()->headers->get('Content-Disposition');
|
$contentDisposition = static::getClient()->getResponse()->headers->get('Content-Disposition');
|
||||||
$this->assertStringContainsString('attachment; filename=', $contentDisposition);
|
$this->assertStringContainsString('attachment; filename=', $contentDisposition);
|
||||||
$this->assertStringContainsString('one-piece-volume-1.cbz', $contentDisposition);
|
$this->assertStringContainsString('one-piece_vol1.cbz', $contentDisposition);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_it_returns_404_when_manga_not_found(): void
|
public function test_it_returns_404_when_manga_not_found(): void
|
||||||
|
|||||||
131
tests/Feature/Manga/ImportChapterTest.php
Normal file
131
tests/Feature/Manga/ImportChapterTest.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Feature\Manga;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
|
class ImportChapterTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private const API_ENDPOINT = '/api/chapters/import';
|
||||||
|
|
||||||
|
public function test_it_returns_404_when_manga_not_found(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$file = $this->createValidCbzFile();
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'mangaId' => 'non-existent-manga-id',
|
||||||
|
'chapterNumber' => '1.5'
|
||||||
|
],
|
||||||
|
['file' => $file]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(404);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertEquals('Manga not found', $response['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_422_when_manga_id_is_missing(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$file = $this->createValidCbzFile();
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'chapterNumber' => '1.5'
|
||||||
|
],
|
||||||
|
['file' => $file]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(422);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertStringContainsString('mangaId is required', $response[0]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_422_when_chapter_number_is_missing(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$file = $this->createValidCbzFile();
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'mangaId' => 'some-manga-id'
|
||||||
|
],
|
||||||
|
['file' => $file]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(422);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertStringContainsString('chapterNumber is required', $response[0]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_422_when_file_is_missing(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'mangaId' => 'some-manga-id',
|
||||||
|
'chapterNumber' => '1.5'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(422);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertStringContainsString('Please upload a file', $response[0]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_422_when_file_is_not_cbz(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Create a non-CBZ file
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_');
|
||||||
|
file_put_contents($tempFile, 'This is not a CBZ file');
|
||||||
|
$file = new UploadedFile($tempFile, 'test.txt', 'text/plain');
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'mangaId' => 'some-manga-id',
|
||||||
|
'chapterNumber' => '1.5'
|
||||||
|
],
|
||||||
|
['file' => $file]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(422);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertStringContainsString('valid CBZ file', $response[0]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createValidCbzFile(): UploadedFile
|
||||||
|
{
|
||||||
|
$tmpFile = tempnam(sys_get_temp_dir(), 'cbz_');
|
||||||
|
unlink($tmpFile);
|
||||||
|
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
if ($zip->open($tmpFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||||
|
throw new \RuntimeException('Cannot create test CBZ file');
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->addFromString('image1.jpg', 'fake-image-data');
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
return new UploadedFile($tmpFile, 'test-chapter.cbz', 'application/x-cbz');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
131
tests/Feature/Manga/ImportVolumeTest.php
Normal file
131
tests/Feature/Manga/ImportVolumeTest.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Feature\Manga;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
|
|
||||||
|
class ImportVolumeTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private const API_ENDPOINT = '/api/volumes/import';
|
||||||
|
|
||||||
|
public function test_it_returns_404_when_manga_not_found(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$file = $this->createValidCbzFile();
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'mangaId' => 'non-existent-manga-id',
|
||||||
|
'volumeNumber' => '1'
|
||||||
|
],
|
||||||
|
['file' => $file]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(404);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertEquals('Manga not found', $response['error']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_422_when_manga_id_is_missing(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$file = $this->createValidCbzFile();
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'volumeNumber' => '1'
|
||||||
|
],
|
||||||
|
['file' => $file]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(422);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertStringContainsString('mangaId is required', $response[0]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_422_when_volume_number_is_missing(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$file = $this->createValidCbzFile();
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'mangaId' => 'some-manga-id'
|
||||||
|
],
|
||||||
|
['file' => $file]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(422);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertStringContainsString('volumeNumber is required', $response[0]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_422_when_file_is_missing(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'mangaId' => 'some-manga-id',
|
||||||
|
'volumeNumber' => '1'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(422);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertStringContainsString('Please upload a file', $response[0]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_returns_422_when_file_is_not_cbz(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
|
||||||
|
// Create a non-CBZ file
|
||||||
|
$tempFile = tempnam(sys_get_temp_dir(), 'test_');
|
||||||
|
file_put_contents($tempFile, 'This is not a CBZ file');
|
||||||
|
$file = new UploadedFile($tempFile, 'test.txt', 'text/plain');
|
||||||
|
|
||||||
|
$client->request(
|
||||||
|
'POST',
|
||||||
|
self::API_ENDPOINT,
|
||||||
|
[
|
||||||
|
'mangaId' => 'some-manga-id',
|
||||||
|
'volumeNumber' => '1'
|
||||||
|
],
|
||||||
|
['file' => $file]
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertResponseStatusCodeSame(422);
|
||||||
|
$response = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertStringContainsString('valid CBZ file', $response[0]['message']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createValidCbzFile(): UploadedFile
|
||||||
|
{
|
||||||
|
$tmpFile = tempnam(sys_get_temp_dir(), 'cbz_');
|
||||||
|
unlink($tmpFile);
|
||||||
|
|
||||||
|
$zip = new \ZipArchive();
|
||||||
|
if ($zip->open($tmpFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
||||||
|
throw new \RuntimeException('Cannot create test CBZ file');
|
||||||
|
}
|
||||||
|
|
||||||
|
$zip->addFromString('image1.jpg', 'fake-image-data');
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
return new UploadedFile($tmpFile, 'test-volume.cbz', 'application/x-cbz');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user