feat(shared) : infra upload générique (ERP-154)
Pose une infra d'upload de fichiers générique et réutilisable dans Shared
(spec M4 § 2.7), sans toucher au module Transport.
- Table uploaded_document (migration racine DoctrineMigrations) : fichier
téléversé immuable (PDF / images), checksum sha256, created_at/created_by.
- Service Shared\Infrastructure\Upload\FileUploader : validation MIME
server-side via getMimeType (jamais getClientMimeType), whitelist explicite
(PDF + images), bornage taille, checksum sha256, écriture var/uploads/{yyyy}/{mm}/.
- Endpoint POST /api/uploaded_documents (multipart, deserialize:false) +
UploadedDocumentProcessor -> renvoie l'IRI ; MIME hors whitelist -> 422.
- COMMENT ON COLUMN sur toutes les colonnes + bloc dans ColumnCommentsCatalog.
- Mapping Doctrine Shared + path API Platform Shared.
- Tests : FileUploader (unit) + endpoint (fonctionnel, 422 / IRI / checksum).
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Shared\Api;
|
||||
|
||||
use App\Shared\Domain\Entity\UploadedDocument;
|
||||
use App\Tests\Module\Core\Api\AbstractApiTestCase;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
/**
|
||||
* Tests fonctionnels de l'endpoint d'upload generique (ERP-154).
|
||||
*
|
||||
* Couvre :
|
||||
* - POST multipart d'un PDF valide -> 201, IRI renvoyee, ligne persistee,
|
||||
* checksum sha256 calcule cote serveur ;
|
||||
* - POST d'un MIME hors whitelist (text/plain) -> 422 ;
|
||||
* - POST sans fichier -> 422 ;
|
||||
* - POST anonyme -> 401 (acces /api protege globalement).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class UploadedDocumentApiTest extends AbstractApiTestCase
|
||||
{
|
||||
private const string ENDPOINT = '/api/uploaded_documents';
|
||||
|
||||
/** @var list<string> */
|
||||
private array $tempFiles = [];
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach ($this->tempFiles as $path) {
|
||||
if (is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testUploadValidPdfReturnsIriAndPersistsRowWithChecksum(): void
|
||||
{
|
||||
$client = $this->authenticatedClient('admin', 'admin');
|
||||
$content = $this->minimalPdf();
|
||||
$file = $this->makeUploadedFile($content, 'facture.pdf');
|
||||
|
||||
$response = $client->request('POST', self::ENDPOINT, [
|
||||
'headers' => ['Accept' => 'application/ld+json'],
|
||||
'extra' => ['files' => ['file' => $file]],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
|
||||
$data = $response->toArray();
|
||||
|
||||
self::assertArrayHasKey('@id', $data);
|
||||
self::assertStringStartsWith(self::ENDPOINT.'/', $data['@id']);
|
||||
self::assertSame('facture.pdf', $data['originalFilename']);
|
||||
self::assertSame('application/pdf', $data['mimeType']);
|
||||
self::assertSame(\strlen($content), $data['sizeBytes']);
|
||||
self::assertSame(hash('sha256', $content), $data['checksum']);
|
||||
self::assertSame(64, \strlen($data['checksum']));
|
||||
|
||||
// La ligne est bien persistee et relisible via le repository.
|
||||
$id = $data['id'];
|
||||
$document = $this->getEm()->getRepository(UploadedDocument::class)->find($id);
|
||||
self::assertInstanceOf(UploadedDocument::class, $document);
|
||||
self::assertSame(hash('sha256', $content), $document->getChecksum());
|
||||
}
|
||||
|
||||
public function testUploadDisallowedMimeTypeReturns422(): void
|
||||
{
|
||||
$client = $this->authenticatedClient('admin', 'admin');
|
||||
$file = $this->makeUploadedFile('just some plain text content', 'note.txt');
|
||||
|
||||
$client->request('POST', self::ENDPOINT, [
|
||||
'headers' => ['Accept' => 'application/ld+json'],
|
||||
'extra' => ['files' => ['file' => $file]],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
}
|
||||
|
||||
public function testUploadWithoutFileReturns422(): void
|
||||
{
|
||||
$client = $this->authenticatedClient('admin', 'admin');
|
||||
|
||||
$client->request('POST', self::ENDPOINT, [
|
||||
'headers' => ['Accept' => 'application/ld+json'],
|
||||
'extra' => ['files' => []],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
}
|
||||
|
||||
public function testUploadAnonymousIsRejected(): void
|
||||
{
|
||||
$client = self::createClient();
|
||||
$file = $this->makeUploadedFile($this->minimalPdf(), 'facture.pdf');
|
||||
|
||||
$client->request('POST', self::ENDPOINT, [
|
||||
'headers' => ['Accept' => 'application/ld+json'],
|
||||
'extra' => ['files' => ['file' => $file]],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(401);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cree un UploadedFile en mode test (move() autorise hors contexte HTTP).
|
||||
*/
|
||||
private function makeUploadedFile(string $content, string $clientName): UploadedFile
|
||||
{
|
||||
$path = sys_get_temp_dir().'/erp154-api-'.bin2hex(random_bytes(4));
|
||||
file_put_contents($path, $content);
|
||||
$this->tempFiles[] = $path;
|
||||
|
||||
return new UploadedFile($path, $clientName, null, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contenu PDF minimal valide (entete `%PDF-1.4` -> finfo `application/pdf`).
|
||||
*/
|
||||
private function minimalPdf(): string
|
||||
{
|
||||
return "%PDF-1.4\n"
|
||||
."1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n"
|
||||
."2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj\n"
|
||||
."3 0 obj<</Type/Page/Parent 2 0 R/MediaBox[0 0 612 792]>>endobj\n"
|
||||
."trailer<</Root 1 0 R/Size 4>>\n"
|
||||
."%%EOF\n";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user