1e783bd753
Auto Tag Develop / tag (push) Successful in 8s
Infra d'upload de fichiers générique et réutilisable dans `Shared` (spec M4 § 2.7). Ne touche pas au module Transport.
## Livré
- **Table `uploaded_document`** (migration racine `DoctrineMigrations`) : fichier téléversé immuable (PDF / images) — `original_filename`, `stored_path`, `mime_type`, `size_bytes`, `checksum` (sha256), `created_at`, `created_by`. COMMENT ON COLUMN sur toutes les colonnes + bloc dans `ColumnCommentsCatalog`.
- **Service `Shared\Infrastructure\Upload\FileUploader`** : validation MIME server-side via `getMimeType()` (jamais `getClientMimeType()`), whitelist explicite (PDF + images), bornage taille (10 Mo), checksum sha256, écriture disque `var/uploads/{yyyy}/{mm}/`.
- **Endpoint `POST /api/uploaded_documents`** (multipart, `deserialize:false`) + `UploadedDocumentProcessor` -> renvoie l'IRI ; MIME hors whitelist -> 422.
- Wiring : mapping Doctrine `Shared` + path API Platform `Shared`.
## Tests
- `FileUploaderTest` (unitaire) + `UploadedDocumentApiTest` (fonctionnel : 201/IRI/checksum, 422 MIME interdit, 422 sans fichier, 401 anonyme).
`make test` vert (701 tests), `php-cs-fixer` propre.
## Hors scope
Pas d'antivirus / S3 / purge (§ 9). Pas de `carrier.discharge_document_id` (ticket consommateur M4).
Ticket ERP-154.
---------
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #108
133 lines
4.4 KiB
PHP
133 lines
4.4 KiB
PHP
<?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";
|
|
}
|
|
}
|