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))); } }