feat(technique) : ProviderProvider + ProviderProcessor + cloisonnement site (ERP-134)
Coeur API du repertoire prestataires (M3), jumeau du M2 fournisseurs : - ProviderProvider : liste paginee (Paginator ORM), filtres search/categoryCode/siteId/includeArchived, tri companyName ASC, exclusion archives + soft-deletes (RG-3.16). Cloisonnement par site pilote par l'utilisateur (RG-3.17 / § 2.13) : liste restreinte au currentSite avant pagination (totalItems = perimetre), detail hors perimetre -> 404, bypass via sites.bypass_scope. - ProviderProcessor : normalisation companyName (RG-3.11), POST formulaire principal (companyName + categories + sites), PATCH partiels par groupe en mode strict (RG-3.15, 403 sur tout le payload), archivage (RG-3.13/3.14), 409 doublon de nom (RG-3.10), garde d'ecriture cloisonnee des sites (RG-3.03/3.17, 422 sur sites pour les users sites.read_ref). - ProviderReadGroupContextBuilder : gating comptabilite par AJOUT du groupe provider:read:accounting si accounting.view (jamais par retrait). - ProviderFieldNormalizer : miroir SupplierFieldNormalizer. - ApiResource cable (provider + processor) sur l'entite Provider. Tests : ProviderApiTest, ProviderListTest, ProviderRbacGatingTest, ProviderSiteScopeTest (26 tests). Suite complete verte (612 tests).
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Technique\Api;
|
||||
|
||||
/**
|
||||
* Tests du cloisonnement par site pilote par l'utilisateur (RG-3.17 / § 2.13) —
|
||||
* ERP-134. Couvre la LECTURE (liste filtree avant pagination + totalItems, detail
|
||||
* 404 hors perimetre, bypass voit tout) et l'ECRITURE (sites hors user_site -> 422).
|
||||
*
|
||||
* Cloisonnement pilote par l'USER (pas le role) : on cree des users non-admin SANS
|
||||
* `sites.bypass_scope`, rattaches a un site precis avec un currentSite. L'admin
|
||||
* (isAdmin -> bypass total) sert de temoin « voit tout ».
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ProviderSiteScopeTest extends AbstractProviderApiTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// Pre-requis : le module Sites doit etre actif (sinon currentSite = null,
|
||||
// cloisonnement no-op et ces tests perdent leur sens).
|
||||
$this->skipIfSitesModuleDisabled();
|
||||
}
|
||||
|
||||
public function testListIsScopedToCurrentSiteForNonBypassUser(): void
|
||||
{
|
||||
$this->seedProvider('Presta Site 86', [self::SITE_86]);
|
||||
$this->seedProvider('Presta Site 17', [self::SITE_17]);
|
||||
$this->seedProvider('Presta Site 82', [self::SITE_82]);
|
||||
|
||||
$creds = $this->createScopedUser(
|
||||
['technique.providers.view'],
|
||||
sitePostalCodes: [self::SITE_86],
|
||||
currentSitePostalCode: self::SITE_86,
|
||||
);
|
||||
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
||||
|
||||
$response = $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertSame(200, $response->getStatusCode());
|
||||
|
||||
$body = $response->toArray();
|
||||
// totalItems reflete le PERIMETRE de l'user (filtre avant pagination).
|
||||
self::assertSame(1, $body['totalItems']);
|
||||
self::assertSame('PRESTA SITE 86', $body['member'][0]['companyName']);
|
||||
}
|
||||
|
||||
public function testDetailOutOfScopeReturns404(): void
|
||||
{
|
||||
$inScope = $this->seedProvider('Dans Perimetre', [self::SITE_86]);
|
||||
$outOfScope = $this->seedProvider('Hors Perimetre', [self::SITE_17]);
|
||||
|
||||
$creds = $this->createScopedUser(
|
||||
['technique.providers.view'],
|
||||
sitePostalCodes: [self::SITE_86],
|
||||
currentSitePostalCode: self::SITE_86,
|
||||
);
|
||||
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
||||
|
||||
// In-scope -> 200.
|
||||
$ok = $client->request('GET', '/api/providers/'.$inScope->getId(), ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertSame(200, $ok->getStatusCode());
|
||||
|
||||
// Out-of-scope -> 404 (ne pas reveler l'existence hors perimetre).
|
||||
$ko = $client->request('GET', '/api/providers/'.$outOfScope->getId(), ['headers' => ['Accept' => self::LD]]);
|
||||
self::assertSame(404, $ko->getStatusCode());
|
||||
}
|
||||
|
||||
public function testBypassUserSeesAllSites(): void
|
||||
{
|
||||
$this->seedProvider('Presta Site 86', [self::SITE_86]);
|
||||
$this->seedProvider('Presta Site 17', [self::SITE_17]);
|
||||
$this->seedProvider('Presta Site 82', [self::SITE_82]);
|
||||
|
||||
// Admin = bypass total.
|
||||
$client = $this->createAdminClient();
|
||||
$response = $client->request('GET', '/api/providers', ['headers' => ['Accept' => self::LD]]);
|
||||
|
||||
self::assertSame(200, $response->getStatusCode());
|
||||
self::assertSame(3, $response->toArray()['totalItems']);
|
||||
}
|
||||
|
||||
public function testWriteOutOfScopeSiteRejectedAtIriResolution(): void
|
||||
{
|
||||
// User non-bypass / non-read_ref : la resolution de l'IRI du site hors
|
||||
// perimetre echoue en amont (SiteCollectionScopedExtension : item Site
|
||||
// « introuvable ») -> 400 anti-enumeration, avant le ProviderProcessor.
|
||||
$creds = $this->createScopedUser(
|
||||
['technique.providers.view', 'technique.providers.manage'],
|
||||
sitePostalCodes: [self::SITE_86],
|
||||
currentSitePostalCode: self::SITE_86,
|
||||
);
|
||||
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
||||
|
||||
$response = $client->request('POST', '/api/providers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => $this->validMainPayload('Hors Scope Sas', [self::SITE_17]),
|
||||
]);
|
||||
|
||||
self::assertSame(400, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testWriteOutOfScopeSiteRejectedByProcessorGuard(): void
|
||||
{
|
||||
// User `sites.read_ref` : peut RESOUDRE n'importe quel site (referentiel
|
||||
// transverse) mais n'opere que sur ses user_site. La garde guardSiteScope
|
||||
// du ProviderProcessor est alors l'enforcement autoritaire de RG-3.17
|
||||
// -> 422 sur `sites` (mappable inline, ERP-101).
|
||||
$creds = $this->createScopedUser(
|
||||
['technique.providers.view', 'technique.providers.manage', 'sites.read_ref'],
|
||||
sitePostalCodes: [self::SITE_86],
|
||||
currentSitePostalCode: self::SITE_86,
|
||||
);
|
||||
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
||||
|
||||
$response = $client->request('POST', '/api/providers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => $this->validMainPayload('Hors Scope Guard', [self::SITE_17]),
|
||||
]);
|
||||
|
||||
self::assertSame(422, $response->getStatusCode());
|
||||
self::assertArrayHasKey('sites', $this->violationsByPath($response->toArray(false)));
|
||||
}
|
||||
|
||||
public function testWriteAllowsSiteWithinUserScope(): void
|
||||
{
|
||||
$creds = $this->createScopedUser(
|
||||
['technique.providers.view', 'technique.providers.manage'],
|
||||
sitePostalCodes: [self::SITE_86],
|
||||
currentSitePostalCode: self::SITE_86,
|
||||
);
|
||||
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
||||
|
||||
// Site 86 = un des user_site -> 201.
|
||||
$response = $client->request('POST', '/api/providers', [
|
||||
'headers' => ['Content-Type' => self::LD],
|
||||
'json' => $this->validMainPayload('Dans Scope Sas', [self::SITE_86]),
|
||||
]);
|
||||
|
||||
self::assertSame(201, $response->getStatusCode());
|
||||
}
|
||||
|
||||
public function testPatchAddingOutOfScopeSiteIsRejected(): void
|
||||
{
|
||||
$provider = $this->seedProvider('Patch Sites', [self::SITE_86]);
|
||||
$id = $provider->getId();
|
||||
|
||||
// read_ref pour pouvoir resoudre l'IRI du site 17 (sinon 400 en amont) et
|
||||
// exercer la garde guardSiteScope sur le PATCH.
|
||||
$creds = $this->createScopedUser(
|
||||
['technique.providers.view', 'technique.providers.manage', 'sites.read_ref'],
|
||||
sitePostalCodes: [self::SITE_86],
|
||||
currentSitePostalCode: self::SITE_86,
|
||||
);
|
||||
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
||||
|
||||
$site86 = $this->site(self::SITE_86)->getId();
|
||||
$site17 = $this->site(self::SITE_17)->getId();
|
||||
|
||||
$response = $client->request('PATCH', '/api/providers/'.$id, [
|
||||
'headers' => ['Content-Type' => self::MERGE],
|
||||
'json' => ['sites' => ['/api/sites/'.$site86, '/api/sites/'.$site17]],
|
||||
]);
|
||||
|
||||
// RG-3.17 : ajouter un site hors user_site -> 422 (garde Processor).
|
||||
self::assertSame(422, $response->getStatusCode());
|
||||
self::assertArrayHasKey('sites', $this->violationsByPath($response->toArray(false)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user