fix: corriger l'erreur HTTP 400 sur les endpoints content-sources POST/PUT

- ContentSourceForm.vue : convertir testChapterNumber en float/null avant
  envoi (évite d'envoyer "" pour ?float, rejeté par Symfony 8 strict)
- UpsertContentSourceResource : ajouter collectDenormalizationErrors: true
  pour que les erreurs de type retournent 422 au lieu de 400 via le
  chemin input: de API Platform 4
- ContentSource entity : corriger setImageSelector(string) → setImageSelector(?string)
  cohérent avec la colonne nullable
- Ajouter les tests manquants (testChapterNumber float/null/chaîne vide)
  qui auraient détecté ces bugs plus tôt
This commit is contained in:
ext.jeremy.guillot@maxicoffee.domains
2026-03-26 18:22:31 +01:00
parent 21d8111734
commit 69c6757cf8
6 changed files with 92 additions and 5 deletions

View File

@@ -242,8 +242,17 @@ watch(() => props.source, (newSource) => {
} }
}, { immediate: true }); }, { immediate: true });
const buildPayload = (formData) => {
const data = { ...formData };
const raw = data.testChapterNumber;
data.testChapterNumber = (raw === '' || raw === null || raw === undefined)
? null
: parseFloat(raw);
return data;
};
const handleSubmit = () => { const handleSubmit = () => {
emit('submit', { ...form.value }); emit('submit', buildPayload(form.value));
}; };
defineExpose({ submitForm: handleSubmit }); defineExpose({ submitForm: handleSubmit });
@@ -252,7 +261,7 @@ const testConfiguration = async () => {
testing.value = true; testing.value = true;
try { try {
await emit('test', { await emit('test', {
configuration: { ...form.value }, configuration: buildPayload(form.value),
testData: { testData: {
mangaSlug: form.value.testSlug, mangaSlug: form.value.testSlug,
chapterNumber: form.value.testChapterNumber, chapterNumber: form.value.testChapterNumber,

View File

@@ -1834,8 +1834,8 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* } * }
* @psalm-type MercureConfig = array{ * @psalm-type MercureConfig = array{
* hubs?: array<string, array{ // Default: [] * hubs?: array<string, array{ // Default: []
* url?: scalar|Param|null, // URL of the hub's publish endpoint * url?: scalar|Param|null, // URL of the hub's publish endpoint // Default: null
* public_url?: scalar|Param|null, // URL of the hub's public endpoint // Default: null * public_url?: scalar|Param|null, // URL of the hub's public endpoint
* jwt?: string|array{ // JSON Web Token configuration. * jwt?: string|array{ // JSON Web Token configuration.
* value?: scalar|Param|null, // JSON Web Token to use to publish to this hub. * value?: scalar|Param|null, // JSON Web Token to use to publish to this hub.
* provider?: scalar|Param|null, // The ID of a service to call to provide the JSON Web Token. * provider?: scalar|Param|null, // The ID of a service to call to provide the JSON Web Token.

View File

@@ -16,6 +16,7 @@ use Symfony\Component\Validator\Constraints as Assert;
uriTemplate: '/content-sources', uriTemplate: '/content-sources',
processor: UpsertContentSourceStateProcessor::class, processor: UpsertContentSourceStateProcessor::class,
input: UpsertContentSourceResource::class, input: UpsertContentSourceResource::class,
collectDenormalizationErrors: true,
status: 201, status: 201,
description: 'Crée une nouvelle source de contenu' description: 'Crée une nouvelle source de contenu'
), ),
@@ -24,6 +25,7 @@ use Symfony\Component\Validator\Constraints as Assert;
provider: GetContentSourceStateProvider::class, provider: GetContentSourceStateProvider::class,
processor: UpsertContentSourceStateProcessor::class, processor: UpsertContentSourceStateProcessor::class,
input: UpsertContentSourceResource::class, input: UpsertContentSourceResource::class,
collectDenormalizationErrors: true,
description: 'Met à jour une source de contenu existante' description: 'Met à jour une source de contenu existante'
), ),
] ]

View File

@@ -73,7 +73,7 @@ class ContentSource
return $this->imageSelector; return $this->imageSelector;
} }
public function setImageSelector(string $imageSelector): static public function setImageSelector(?string $imageSelector): static
{ {
$this->imageSelector = $imageSelector; $this->imageSelector = $imageSelector;

View File

@@ -181,4 +181,35 @@ final class CreateContentSourceTest extends AbstractApiTestCase
$this->assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY); $this->assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY);
} }
public function testItAcceptsTestChapterNumberAsFloat(): void
{
$response = static::createClient()->request('POST', '/api/content-sources', [
'json' => [
'baseUrl' => 'https://mangadex.org',
'chapterUrlFormat' => 'https://mangadex.org/chapter/{id}',
'scrapingType' => 'html',
'testSlug' => 'one-piece',
'testChapterNumber' => 1.5,
],
]);
$this->assertResponseStatusCodeSame(Response::HTTP_CREATED);
}
public function testItRejectsTestChapterNumberAsEmptyStringWith422(): void
{
// Cas réel du formulaire Vue : le champ vide envoie "" au lieu de null.
// Doit retourner 422 (erreur de validation) et non 400 (données malformées).
$response = static::createClient()->request('POST', '/api/content-sources', [
'json' => [
'baseUrl' => 'https://mangadex.org',
'chapterUrlFormat' => 'https://mangadex.org/chapter/{id}',
'scrapingType' => 'html',
'testChapterNumber' => '',
],
]);
$this->assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY);
}
} }

View File

@@ -188,4 +188,49 @@ final class UpdateContentSourceTest extends AbstractApiTestCase
$this->assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY); $this->assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY);
} }
public function testItAcceptsTestChapterNumberAsFloat(): void
{
$response = static::createClient()->request('PUT', "/api/content-sources/{$this->sourceId}", [
'json' => [
'baseUrl' => 'https://mangadex.org',
'chapterUrlFormat' => 'https://mangadex.org/chapter/{id}',
'scrapingType' => 'html',
'testSlug' => 'one-piece',
'testChapterNumber' => 1.5,
],
]);
$this->assertResponseIsSuccessful();
}
public function testItAcceptsTestChapterNumberAsNull(): void
{
$response = static::createClient()->request('PUT', "/api/content-sources/{$this->sourceId}", [
'json' => [
'baseUrl' => 'https://mangadex.org',
'chapterUrlFormat' => 'https://mangadex.org/chapter/{id}',
'scrapingType' => 'html',
'testChapterNumber' => null,
],
]);
$this->assertResponseIsSuccessful();
}
public function testItRejectsTestChapterNumberAsEmptyStringWith422(): void
{
// Cas réel du formulaire Vue : le champ vide envoie "" au lieu de null.
// Doit retourner 422 (erreur de validation) et non 400 (données malformées).
$response = static::createClient()->request('PUT', "/api/content-sources/{$this->sourceId}", [
'json' => [
'baseUrl' => 'https://mangadex.org',
'chapterUrlFormat' => 'https://mangadex.org/chapter/{id}',
'scrapingType' => 'html',
'testChapterNumber' => '',
],
]);
$this->assertResponseStatusCodeSame(Response::HTTP_UNPROCESSABLE_ENTITY);
}
} }