0ca1fb159a
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).
160 lines
6.4 KiB
PHP
160 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Technique\Api;
|
|
|
|
use App\Module\Technique\Domain\Entity\Provider;
|
|
|
|
/**
|
|
* Tests du gating comptabilite + mode strict par groupe (ProviderProcessor /
|
|
* ProviderReadGroupContextBuilder) — ERP-134.
|
|
*
|
|
* Couvre : gating accounting PAR OMISSION (siren/ribs absents sans accounting.view,
|
|
* bug #4 M1), mode strict RG-3.15 (403 sur tout le payload), gating archive (RG-3.13).
|
|
*
|
|
* Les users sont crees via createUserWithPermissions() (parent) : rattaches a TOUS
|
|
* les sites SANS currentSite -> CurrentSiteProvider::get() = null -> aucun
|
|
* cloisonnement, on isole ainsi le comportement RBAC du comportement site.
|
|
*
|
|
* @internal
|
|
*/
|
|
final class ProviderRbacGatingTest extends AbstractProviderApiTestCase
|
|
{
|
|
public function testAccountingFieldsOmittedWithoutAccountingView(): void
|
|
{
|
|
$provider = $this->seedProvider('Compta Masquee', [self::SITE_86], siren: '123456789');
|
|
$id = $provider->getId();
|
|
|
|
// Profil type Commerciale : view + manage SANS accounting.view.
|
|
$creds = $this->createUserWithPermissions(['technique.providers.view']);
|
|
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
|
|
|
$response = $client->request('GET', '/api/providers/'.$id, ['headers' => ['Accept' => self::LD]]);
|
|
self::assertSame(200, $response->getStatusCode());
|
|
|
|
$body = $response->toArray();
|
|
// Gating par omission : scalaires comptables ET ribs totalement absents.
|
|
self::assertArrayNotHasKey('siren', $body);
|
|
self::assertArrayNotHasKey('ribs', $body);
|
|
// isArchived reste expose (bug #3 M1 : la cle ne doit pas etre droppee).
|
|
self::assertArrayHasKey('isArchived', $body);
|
|
}
|
|
|
|
public function testAccountingFieldsPresentWithAccountingView(): void
|
|
{
|
|
$provider = $this->seedProvider('Compta Visible', [self::SITE_86], siren: '987654321');
|
|
$id = $provider->getId();
|
|
|
|
$creds = $this->createUserWithPermissions([
|
|
'technique.providers.view',
|
|
'technique.providers.accounting.view',
|
|
]);
|
|
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
|
|
|
$response = $client->request('GET', '/api/providers/'.$id, ['headers' => ['Accept' => self::LD]]);
|
|
self::assertSame(200, $response->getStatusCode());
|
|
|
|
$body = $response->toArray();
|
|
self::assertSame('987654321', $body['siren']);
|
|
// La cle ribs apparait (collection vide ici, mais presente).
|
|
self::assertArrayHasKey('ribs', $body);
|
|
}
|
|
|
|
public function testStrictModeRejectsMixedGroupsForManageOnlyUser(): void
|
|
{
|
|
$provider = $this->seedProvider('Strict Cible', [self::SITE_86]);
|
|
$id = $provider->getId();
|
|
|
|
// Profil type Bureau : manage SANS accounting.manage.
|
|
$creds = $this->createUserWithPermissions([
|
|
'technique.providers.view',
|
|
'technique.providers.manage',
|
|
]);
|
|
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
|
|
|
$response = $client->request('PATCH', '/api/providers/'.$id, [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['companyName' => 'Renomme', 'siren' => '111222333'],
|
|
]);
|
|
|
|
// RG-3.15 : payload melangeant main + accounting sans accounting.manage
|
|
// -> 403 sur tout le payload (mode strict, pas de filtrage silencieux).
|
|
self::assertSame(403, $response->getStatusCode());
|
|
|
|
// Aucun champ n'a ete persiste (rollback du mode strict).
|
|
$this->getEm()->clear();
|
|
$reloaded = $this->getEm()->getRepository(Provider::class)->find($id);
|
|
self::assertSame('STRICT CIBLE', $reloaded->getCompanyName());
|
|
self::assertNull($reloaded->getSiren());
|
|
}
|
|
|
|
public function testAccountingOnlyUserCanPatchAccountingButNotMain(): void
|
|
{
|
|
$provider = $this->seedProvider('Compta Editrice', [self::SITE_86]);
|
|
$id = $provider->getId();
|
|
|
|
// Profil type Compta : accounting.view + accounting.manage SANS manage.
|
|
$creds = $this->createUserWithPermissions([
|
|
'technique.providers.view',
|
|
'technique.providers.accounting.view',
|
|
'technique.providers.accounting.manage',
|
|
]);
|
|
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
|
|
|
// PATCH accounting -> 200.
|
|
$ok = $client->request('PATCH', '/api/providers/'.$id, [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['siren' => '555666777'],
|
|
]);
|
|
self::assertSame(200, $ok->getStatusCode());
|
|
|
|
// PATCH main (companyName) -> 403 (pas de permission manage).
|
|
$ko = $client->request('PATCH', '/api/providers/'.$id, [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['companyName' => 'Interdit'],
|
|
]);
|
|
self::assertSame(403, $ko->getStatusCode());
|
|
}
|
|
|
|
public function testArchiveRequiresArchivePermission(): void
|
|
{
|
|
$provider = $this->seedProvider('A Archiver', [self::SITE_86]);
|
|
$id = $provider->getId();
|
|
|
|
// Bureau (manage) sans archive -> 403.
|
|
$creds = $this->createUserWithPermissions([
|
|
'technique.providers.view',
|
|
'technique.providers.manage',
|
|
]);
|
|
$client = $this->authenticatedClient($creds['username'], $creds['password']);
|
|
|
|
$response = $client->request('PATCH', '/api/providers/'.$id, [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['isArchived' => true],
|
|
]);
|
|
|
|
// RG-3.13 : l'archivage exige technique.providers.archive.
|
|
self::assertSame(403, $response->getStatusCode());
|
|
}
|
|
|
|
public function testAdminCanArchiveAndSetsArchivedAt(): void
|
|
{
|
|
$provider = $this->seedProvider('Archivable', [self::SITE_86]);
|
|
$id = $provider->getId();
|
|
|
|
$client = $this->createAdminClient();
|
|
$response = $client->request('PATCH', '/api/providers/'.$id, [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['isArchived' => true],
|
|
]);
|
|
|
|
self::assertSame(200, $response->getStatusCode());
|
|
|
|
$this->getEm()->clear();
|
|
$reloaded = $this->getEm()->getRepository(Provider::class)->find($id);
|
|
self::assertTrue($reloaded->isArchived());
|
|
self::assertNotNull($reloaded->getArchivedAt());
|
|
}
|
|
}
|