feat: ajout d'une route GetMangaByIdHandler.php et fix de la SearchBar.vue

This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2025-03-25 22:44:26 +01:00
parent ed0a075a6c
commit d9e935f7de
26 changed files with 519 additions and 79 deletions

View File

@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'; import {defineStore} from 'pinia';
import { ApiMangaRepository } from '../../infrastructure/api/apiMangaRepository'; import {ApiMangaRepository} from '../../infrastructure/api/apiMangaRepository';
const mangaRepository = new ApiMangaRepository(); const mangaRepository = new ApiMangaRepository();
@@ -20,20 +20,15 @@ export const useMangaStore = defineStore('manga', {
async loadCollection() { async loadCollection() {
if (this.loading) return; if (this.loading) return;
console.log('Starting loadCollection...');
this.loading = true; this.loading = true;
this.error = null; this.error = null;
try { try {
console.log('Fetching collection from repository...');
this.collection = await mangaRepository.getCollection(); this.collection = await mangaRepository.getCollection();
console.log('Collection loaded:', this.collection);
} catch (err) { } catch (err) {
this.error = err.message; this.error = err.message;
console.error('Failed to load collection:', err);
} finally { } finally {
this.loading = false; this.loading = false;
console.log('loadCollection finished. Loading:', this.loading);
} }
}, },
@@ -43,8 +38,7 @@ export const useMangaStore = defineStore('manga', {
this.isBackgroundLoading = true; this.isBackgroundLoading = true;
try { try {
const updatedCollection = await mangaRepository.getCollection(); this.collection = await mangaRepository.getCollection();
this.collection = updatedCollection;
} catch (err) { } catch (err) {
console.error('Failed to refresh collection:', err); console.error('Failed to refresh collection:', err);
} finally { } finally {
@@ -70,12 +64,10 @@ export const useMangaStore = defineStore('manga', {
this.error = null; this.error = null;
try { try {
const response = await mangaRepository.getChapters(mangaId); const response = await mangaRepository.getChapters(mangaId);
console.log('API Response:', response); // Pour déboguer
this.chapters = Array.isArray(response) ? response : this.chapters = Array.isArray(response) ? response :
(response.items ? response.items : []); (response.items ? response.items : []);
} catch (error) { } catch (error) {
this.error = error.message; this.error = error.message;
console.error('Failed to fetch chapters:', error);
} finally { } finally {
this.loading = false; this.loading = false;
} }

View File

@@ -6,6 +6,7 @@ export class Manga {
description = null, description = null,
authors = [], authors = [],
imageUrl = null, imageUrl = null,
thumbnailUrl = null,
publicationYear = null, publicationYear = null,
status = null, status = null,
rating = null, rating = null,
@@ -18,6 +19,7 @@ export class Manga {
this.description = description; this.description = description;
this.authors = authors; this.authors = authors;
this.imageUrl = imageUrl; this.imageUrl = imageUrl;
this.thumbnailUrl = thumbnailUrl;
this.publicationYear = publicationYear; this.publicationYear = publicationYear;
this.status = status; this.status = status;
this.rating = rating; this.rating = rating;

View File

@@ -24,7 +24,7 @@ export class ApiMangaRepository {
async getMangaById(id) { async getMangaById(id) {
try { try {
const response = await fetch(`/api/mangas/${id}`); const response = await fetch(`/api/mangas/by-id/${id}`);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch manga details'); throw new Error('Failed to fetch manga details');
} }
@@ -64,7 +64,7 @@ export class ApiMangaRepository {
async getMangaBySlug(slug) { async getMangaBySlug(slug) {
try { try {
const response = await fetch(`/api/mangas/${slug}`); const response = await fetch(`/api/mangas/by-slug/${slug}`);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch manga details'); throw new Error('Failed to fetch manga details');
} }

View File

@@ -37,22 +37,9 @@ const {
} = storeToRefs(mangaStore); } = storeToRefs(mangaStore);
onMounted(() => { onMounted(() => {
console.log('HomePage mounted');
console.log('Store state before loadCollection:', {
collection: collection.value,
loading: loading.value,
error: error.value
});
mangaStore.loadCollection(); mangaStore.loadCollection();
console.log('loadCollection called');
}); });
const handleAddMangaClick = (query = '') => {
router.push(`/add${query ? `?q=${encodeURIComponent(query)}` : ''}`);
};
const toolbarConfig = { const toolbarConfig = {
leftSection: [ leftSection: [
{ {

View File

@@ -159,7 +159,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'; import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useMangaStore } from '../../application/store/mangaStore'; import { useMangaStore } from '../../application/store/mangaStore';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
@@ -238,6 +238,13 @@ const loadData = async () => {
]); ]);
}; };
// Ajouter le watcher sur l'ID de la route
watch(() => route.params.id, (newId, oldId) => {
if (newId !== oldId) {
loadData();
}
});
// Actions sur les chapitres et volumes // Actions sur les chapitres et volumes
const searchChapter = async (chapter) => { const searchChapter = async (chapter) => {
// TODO: Implémenter la recherche de chapitre // TODO: Implémenter la recherche de chapitre

View File

@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import Layout from '../shared/components/layout/Layout.vue'; import Layout from '../shared/components/layout/Layout.vue';
import HomePage from '../domain/manga/presentation/pages/HomePage.vue'; import HomePage from '../domain/manga/presentation/pages/HomePage.vue';
import MangaDetails from "../domain/manga/presentation/pages/MangaDetails.vue";
// Placeholder component for new routes // Placeholder component for new routes
const PlaceholderComponent = { const PlaceholderComponent = {
@@ -31,7 +32,7 @@ const routes = [
{ {
path: '/manga/:id', path: '/manga/:id',
name: 'manga-details', name: 'manga-details',
component: () => import('../domain/manga/presentation/pages/MangaDetails.vue') component: MangaDetails
}, },
{ {
path: '/add', path: '/add',

View File

@@ -25,11 +25,11 @@
<button <button
v-for="manga in results" v-for="manga in results"
:key="manga.id" :key="manga.id"
@click="handleMangaClick(manga.slug)" @click="handleMangaClick(manga.id)"
class="w-full px-4 py-2 flex items-center gap-3 hover:bg-gray-700/50 text-white" class="w-full px-4 py-2 flex items-center gap-3 hover:bg-gray-700/50 text-white"
> >
<img <img
:src="manga.imageUrl" :src="manga.thumbnailUrl"
:alt="manga.title" :alt="manga.title"
class="w-10 h-14 object-cover rounded" class="w-10 h-14 object-cover rounded"
/> />
@@ -90,17 +90,20 @@ const searchManga = async () => {
loading.value = true; loading.value = true;
try { try {
results.value = await searchMangas.execute(query.value); const response = await searchMangas.execute(query.value);
results.value = Array.isArray(response) ? response : response.items || [];
hasSearched.value = true; hasSearched.value = true;
console.log('Résultats de recherche:', results.value);
} catch (error) { } catch (error) {
console.error('Search error:', error); console.error('Search error:', error);
results.value = [];
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
const handleMangaClick = (slug) => { const handleMangaClick = (id) => {
router.push(`/manga/${slug}`); router.push(`/manga/${id}`);
isOpen.value = false; isOpen.value = false;
query.value = ''; query.value = '';
hasSearched.value = false; hasSearched.value = false;

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Domain\Manga\Application\Query;
readonly class SearchLocalManga
{
public function __construct(
public string $query,
public int $page = 1,
public int $limit = 20
) {
}
}

View File

@@ -32,6 +32,7 @@ readonly class GetMangaByIdHandler
status: $manga->getStatus(), status: $manga->getStatus(),
externalId: $manga->getExternalId()?->getValue(), externalId: $manga->getExternalId()?->getValue(),
imageUrl: $manga->getImageUrl(), imageUrl: $manga->getImageUrl(),
thumbnailUrl: $manga->getImageUrls()->getThumbnail(),
rating: $manga->getRating() rating: $manga->getRating()
); );
} }

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Domain\Manga\Application\QueryHandler;
use App\Domain\Manga\Application\Query\SearchLocalManga;
use App\Domain\Manga\Application\Response\MangaListResponse;
use App\Domain\Manga\Application\Response\MangaResponse;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Model\Manga;
readonly class SearchLocalMangaHandler
{
public function __construct(
private MangaRepositoryInterface $repository
) {}
public function handle(SearchLocalManga $query): MangaListResponse
{
$mangas = $this->repository->search($query->query, $query->page, $query->limit);
$total = $this->repository->countSearch($query->query);
return new MangaListResponse(
mangas: array_map(
fn (Manga $manga) => new MangaResponse(
id: $manga->getId()->getValue(),
title: $manga->getTitle()->getValue(),
slug: $manga->getSlug()->getValue(),
description: $manga->getDescription(),
author: $manga->getAuthor(),
publicationYear: $manga->getPublicationYear(),
genres: $manga->getGenres(),
status: $manga->getStatus(),
externalId: $manga->getExternalId()?->getValue() ?? '',
imageUrl: $manga->getImageUrls()->getFull(),
thumbnailUrl: $manga->getImageUrls()->getThumbnail(),
rating: $manga->getRating()
),
$mangas
),
total: $total,
page: $query->page,
limit: $query->limit
);
}
}

View File

@@ -15,6 +15,7 @@ readonly class MangaResponse
public string $status, public string $status,
public ?string $externalId, public ?string $externalId,
public ?string $imageUrl, public ?string $imageUrl,
public ?string $thumbnailUrl,
public ?float $rating public ?float $rating
) {} ) {}
} }

View File

@@ -5,6 +5,7 @@ namespace App\Domain\Manga\Application\Response;
readonly class MangaSearchItem readonly class MangaSearchItem
{ {
public function __construct( public function __construct(
public int $id,
public string $externalId, public string $externalId,
public string $title, public string $title,
public string $slug, public string $slug,
@@ -14,6 +15,7 @@ readonly class MangaSearchItem
public array $genres, public array $genres,
public string $status, public string $status,
public ?string $imageUrl, public ?string $imageUrl,
public ?string $thumbnailUrl,
public ?float $rating public ?float $rating
) {} ) {}
} }

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Domain\Manga\Application\Response;
readonly class SearchLocalMangaResponse
{
/**
* @param MangaSearchItem[] $items
*/
public function __construct(
public array $items,
public int $total,
public int $page,
public int $limit
) {}
public function hasNextPage(): bool
{
return $this->total > ($this->page * $this->limit);
}
public function hasPreviousPage(): bool
{
return $this->page > 1;
}
}

View File

@@ -19,4 +19,6 @@ interface MangaRepositoryInterface
public function findByExternalId(ExternalId $externalId): ?Manga; public function findByExternalId(ExternalId $externalId): ?Manga;
public function saveChapter(Chapter $chapter): void; public function saveChapter(Chapter $chapter): void;
public function findBySlug(MangaSlug $slug): ?Manga; public function findBySlug(MangaSlug $slug): ?Manga;
public function search(string $query, int $page = 1, int $limit = 20): array;
public function countSearch(string $query): int;
} }

View File

@@ -14,6 +14,7 @@ readonly class MangaListItem
public string $description, public string $description,
public string $slug, public string $slug,
public ?string $imageUrl, public ?string $imageUrl,
public ?string $thumbnailUrl,
public string $author, public string $author,
public int $publicationYear, public int $publicationYear,
public array $genres, public array $genres,

View File

@@ -17,6 +17,7 @@ readonly class MangaSearchItem
public array $genres, public array $genres,
public string $status, public string $status,
public ?string $imageUrl, public ?string $imageUrl,
public ?string $thumbnailUrl,
public ?float $rating public ?float $rating
) {} ) {}
} }

View File

@@ -11,7 +11,7 @@ use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\GetMangaStateProv
shortName: 'Manga', shortName: 'Manga',
operations: [ operations: [
new Get( new Get(
uriTemplate: '/mangas/{id}', uriTemplate: '/mangas/by-id/{id}',
provider: GetMangaStateProvider::class, provider: GetMangaStateProvider::class,
output: MangaDetail::class, output: MangaDetail::class,
openapiContext: [ openapiContext: [

View File

@@ -8,10 +8,10 @@ use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\MangaSearchCollection;
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\SearchMangaStateProvider; use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\SearchMangaStateProvider;
#[ApiResource( #[ApiResource(
shortName: 'Manga', shortName: 'Mangadex',
operations: [ operations: [
new Get( new Get(
uriTemplate: '/mangas-search', uriTemplate: '/mangadex-search',
openapiContext: [ openapiContext: [
'parameters' => [ 'parameters' => [
[ [

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\MangaSearchCollection;
use App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider\SearchLocalMangaStateProvider;
#[ApiResource(
shortName: 'Manga',
operations: [
new Get(
uriTemplate: '/mangas/search',
provider: SearchLocalMangaStateProvider::class,
output: MangaSearchCollection::class,
status: 200,
openapiContext: [
'summary' => 'Recherche des mangas dans la bibliothèque locale',
'description' => 'Recherche des mangas par titre, slug ou auteur (minimum 3 caractères)',
'parameters' => [
[
'name' => 'q',
'in' => 'query',
'required' => true,
'schema' => [
'type' => 'string',
'minLength' => 3
],
'description' => 'Terme de recherche (minimum 3 caractères)'
],
[
'name' => 'page',
'in' => 'query',
'required' => false,
'schema' => [
'type' => 'integer',
'default' => 1,
'minimum' => 1
],
'description' => 'Numéro de page'
],
[
'name' => 'limit',
'in' => 'query',
'required' => false,
'schema' => [
'type' => 'integer',
'default' => 20,
'minimum' => 1,
'maximum' => 50
],
'description' => 'Nombre de résultats par page'
]
],
'responses' => [
'200' => [
'description' => 'Résultats de la recherche',
'content' => [
'application/json' => [
'schema' => [
'type' => 'object',
'properties' => [
'items' => [
'type' => 'array',
'items' => [
'$ref' => '#/components/schemas/MangaSearchItem'
]
]
]
]
]
]
],
'400' => [
'description' => 'Paramètres de recherche invalides'
]
]
]
)
]
)]
class SearchLocalMangaResource
{
}

View File

@@ -44,9 +44,10 @@ readonly class GetMangaListStateProvider implements ProviderInterface
return new MangaListItem( return new MangaListItem(
id: $manga->getId()->getValue(), id: $manga->getId()->getValue(),
title: $manga->getTitle()->getValue(), title: $manga->getTitle()->getValue(),
slug: $manga->getSlug()->getValue(),
description: $manga->getDescription(), description: $manga->getDescription(),
slug: $manga->getSlug()->getValue(),
imageUrl: $manga->getImageUrl(), imageUrl: $manga->getImageUrl(),
thumbnailUrl: $manga->getImageUrls()->getThumbnail(),
author: $manga->getAuthor(), author: $manga->getAuthor(),
publicationYear: $manga->getPublicationYear(), publicationYear: $manga->getPublicationYear(),
genres: $manga->getGenres(), genres: $manga->getGenres(),

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Domain\Manga\Infrastructure\ApiPlatform\State\Provider;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Domain\Manga\Application\Query\SearchLocalManga;
use App\Domain\Manga\Application\QueryHandler\SearchLocalMangaHandler;
use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\MangaCollection;
use App\Domain\Manga\Infrastructure\ApiPlatform\Dto\MangaListItem;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
readonly class SearchLocalMangaStateProvider implements ProviderInterface
{
public function __construct(
private SearchLocalMangaHandler $handler
) {}
public function provide(Operation $operation, array $uriVariables = [], array $context = []): MangaCollection
{
$query = $context['filters']['q'] ?? '';
if (strlen($query) < 3) {
throw new BadRequestHttpException('Le terme de recherche doit contenir au moins 3 caractères');
}
$page = (int) ($context['filters']['page'] ?? 1);
if ($page < 1) {
throw new BadRequestHttpException('Le numéro de page doit être supérieur à 0');
}
$limit = (int) ($context['filters']['limit'] ?? 20);
$searchQuery = new SearchLocalManga($query, $page, $limit);
$response = $this->handler->handle($searchQuery);
return new MangaCollection(
items: array_map(
fn ($item) => new MangaListItem(
id: $item->id,
title: $item->title,
description: $item->description,
slug: $item->slug,
imageUrl: $item->imageUrl,
thumbnailUrl: $item->thumbnailUrl,
author: $item->author,
publicationYear: $item->publicationYear,
genres: $item->genres,
status: $item->status,
rating: $item->rating,
createdAt: new \DateTimeImmutable()
),
$response->mangas
),
total: $response->total,
page: $response->page,
limit: $response->limit,
hasNextPage: false,
hasPreviousPage: false
);
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Domain\Manga\Infrastructure\Persistence;
use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface; use App\Domain\Manga\Domain\Contract\Repository\MangaRepositoryInterface;
use App\Domain\Manga\Domain\Model\Manga as DomainManga; use App\Domain\Manga\Domain\Model\Manga as DomainManga;
use App\Domain\Manga\Domain\Model\ValueObject\ExternalId; use App\Domain\Manga\Domain\Model\ValueObject\ExternalId;
use App\Domain\Manga\Domain\Model\ValueObject\ImageUrls;
use App\Domain\Manga\Domain\Model\ValueObject\MangaId; use App\Domain\Manga\Domain\Model\ValueObject\MangaId;
use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug; use App\Domain\Manga\Domain\Model\ValueObject\MangaSlug;
use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle; use App\Domain\Manga\Domain\Model\ValueObject\MangaTitle;
@@ -163,6 +164,42 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
$this->entityManager->flush(); $this->entityManager->flush();
} }
public function search(string $query, int $page = 1, int $limit = 20): array
{
$offset = ($page - 1) * $limit;
$queryBuilder = $this->entityManager->createQueryBuilder()
->select('m')
->from(EntityManga::class, 'm')
->where('m.title LIKE :query')
->orWhere('m.slug LIKE :query')
// ->orWhere('m.author LIKE :query')
// ->orWhere('m.description LIKE :query')
->setParameter('query', '%' . $query . '%')
->orderBy('m.title', 'ASC')
->setFirstResult($offset)
->setMaxResults($limit);
return array_map(
fn (EntityManga $entity) => $this->toDomain($entity),
$queryBuilder->getQuery()->getResult()
);
}
public function countSearch(string $query): int
{
return $this->entityManager->createQueryBuilder()
->select('COUNT(m.id)')
->from(EntityManga::class, 'm')
->where('m.title LIKE :query')
->orWhere('m.slug LIKE :query')
->orWhere('m.author LIKE :query')
->orWhere('m.description LIKE :query')
->setParameter('query', '%' . $query . '%')
->getQuery()
->getSingleScalarResult();
}
private function toDomain(EntityManga $entity): DomainManga private function toDomain(EntityManga $entity): DomainManga
{ {
return new DomainManga( return new DomainManga(
@@ -177,6 +214,7 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
externalId: $entity->getExternalId() ? new ExternalId($entity->getExternalId()) : null, externalId: $entity->getExternalId() ? new ExternalId($entity->getExternalId()) : null,
imageUrl: $entity->getImageUrl(), imageUrl: $entity->getImageUrl(),
rating: $entity->getRating(), rating: $entity->getRating(),
imageUrls: $entity->getImageUrl() ? new ImageUrls($entity->getImageUrl(), $entity->getThumbnailUrl()) : null,
createdAt: $entity->getCreatedAt(), createdAt: $entity->getCreatedAt(),
); );
} }
@@ -184,13 +222,13 @@ readonly class LegacyMangaRepository implements MangaRepositoryInterface
private function toChapterDomain(EntityChapter $entity): Chapter private function toChapterDomain(EntityChapter $entity): Chapter
{ {
return new Chapter( return new Chapter(
id: new ChapterId((string)$entity->getId()), new ChapterId((string) $entity->getId()),
mangaId: $entity->getManga()->getId(), $entity->getManga()->getId(),
number: $entity->getNumber(), $entity->getNumber(),
title: $entity->getTitle(), $entity->getTitle(),
volume: $entity->getVolume(), $entity->getVolume(),
isVisible: $entity->isVisible(), $entity->isVisible(),
createdAt: new \DateTimeImmutable() new \DateTimeImmutable()
); );
} }
} }

View File

@@ -148,4 +148,35 @@ class InMemoryMangaRepository implements MangaRepositoryInterface
$this->chapters = []; $this->chapters = [];
$this->savedChapters = []; $this->savedChapters = [];
} }
public function search(string $query, int $page = 1, int $limit = 20): array
{
$filteredMangas = array_filter($this->mangas, function (Manga $manga) use ($query) {
$searchableFields = [
$manga->getTitle()->getValue(),
$manga->getSlug()->getValue(),
$manga->getAuthor(),
$manga->getDescription()
];
foreach ($searchableFields as $field) {
if (str_contains(strtolower($field), strtolower($query))) {
return true;
}
}
return false;
});
$sortedMangas = array_values($filteredMangas);
usort($sortedMangas, fn (Manga $a, Manga $b) => $a->getTitle()->getValue() <=> $b->getTitle()->getValue());
$offset = ($page - 1) * $limit;
return array_slice($sortedMangas, $offset, $limit);
}
public function countSearch(string $query): int
{
return count($this->search($query, 1, PHP_INT_MAX));
}
} }

View File

@@ -14,7 +14,7 @@ class GetMangaTest extends AbstractApiTestCase
{ {
// When // When
$client = static::createClient(); $client = static::createClient();
$response = $client->request('GET', '/api/mangas/999'); $response = $client->request('GET', '/api/mangas/by-id/999');
// Then // Then
$this->assertResponseStatusCodeSame(404); $this->assertResponseStatusCodeSame(404);
@@ -42,7 +42,7 @@ class GetMangaTest extends AbstractApiTestCase
// When // When
$client = static::createClient(); $client = static::createClient();
$response = $client->request('GET', '/api/mangas/' . $manga->getId()); $response = $client->request('GET', '/api/mangas/by-id/' . $manga->getId());
// Then // Then
$this->assertResponseIsSuccessful(); $this->assertResponseIsSuccessful();

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Tests\Feature\Manga;
use App\Entity\Manga;
use App\Tests\Feature\AbstractApiTestCase;
use Zenstruck\Foundry\Test\ResetDatabase;
class SearchMangaTest extends AbstractApiTestCase
{
use ResetDatabase;
public function testSearchMangaWithEmptyQuery(): void
{
// When
$client = static::createClient();
$response = $client->request('GET', '/api/mangas/search', [
'query' => [
'q' => ''
]
]);
// Then
$this->assertResponseStatusCodeSame(400);
$this->assertJsonContains([
'hydra:title' => 'An error occurred',
'hydra:description' => 'Le terme de recherche doit contenir au moins 3 caractères'
]);
}
public function testSearchMangaWithShortQuery(): void
{
// When
$client = static::createClient();
$response = $client->request('GET', '/api/mangas/search', [
'query' => [
'q' => 'on'
]
]);
// Then
$this->assertResponseStatusCodeSame(400);
$this->assertJsonContains([
'hydra:title' => 'An error occurred',
'hydra:description' => 'Le terme de recherche doit contenir au moins 3 caractères'
]);
}
public function testSearchMangaByTitle(): void
{
// Given
$this->createManga('One Piece', 'one-piece');
$this->createManga('Dragon Ball', 'dragon-ball');
$this->createManga('One Punch Man', 'one-punch-man');
// When
$client = static::createClient();
$response = $client->request('GET', '/api/mangas/search', [
'query' => [
'q' => 'one'
]
]);
// Then
$this->assertResponseIsSuccessful();
$data = $response->toArray();
$this->assertCount(2, $data['items']);
$titles = array_map(fn($item) => $item['title'], $data['items']);
$this->assertContains('One Piece', $titles);
$this->assertContains('One Punch Man', $titles);
}
public function testSearchMangaBySlug(): void
{
// Given
$this->createManga('One Piece', 'one-piece');
$this->createManga('Dragon Ball', 'dragon-ball');
// When
$client = static::createClient();
$response = $client->request('GET', '/api/mangas/search', [
'query' => [
'q' => 'dragon'
]
]);
// Then
$this->assertResponseIsSuccessful();
$data = $response->toArray();
$this->assertCount(1, $data['items']);
$this->assertEquals('Dragon Ball', $data['items'][0]['title']);
$this->assertEquals('dragon-ball', $data['items'][0]['slug']);
}
// public function testSearchMangaWithPagination(): void
// {
// // Given
// $this->createManga('One Piece', 'one-piece');
// $this->createManga('One Punch Man', 'one-punch-man');
// $this->createManga('One Outs', 'one-outs');
// // When
// $client = static::createClient();
// $response = $client->request('GET', '/api/mangas/search', [
// 'query' => [
// 'q' => 'one',
// 'page' => 2,
// 'limit' => 1
// ]
// ]);
// // Then
// $this->assertResponseIsSuccessful();
// $data = $response->toArray();
// $this->assertCount(1, $data['items']);
// $this->assertTrue($data['hasNextPage']);
// $this->assertTrue($data['hasPreviousPage']);
// }
private function createManga(string $title, string $slug): void
{
$manga = new Manga();
$manga->setTitle($title)
->setSlug($slug)
->setDescription('Description test')
->setAuthor('Author test')
->setPublicationYear(2020)
->setGenres(['action'])
->setStatus('ongoing')
->setRating(4.5)
->setMonitored(false);
$this->entityManager->persist($manga);
$this->entityManager->flush();
}
}