3fe0f676f6
Auto Tag Develop / tag (push) Successful in 11s
Ticket Lesstime #139 (M3 — Répertoire prestataires, position 1.9). DoD back avant le front : suite PHPUnit consolidée sur la matrice § 8.1 + captures JSON réelles dans la spec § 4.0.bis. ## Contenu - **Fix réfs comptables** : `provider:read:accounting` ajouté sur `TvaMode`/`PaymentDelay`/`PaymentType`/`Bank` — sans ça elles sortaient en IRI nu dans le détail prestataire (réplique du fix ERP-92 du M2, piège #1 § 4.0.bis). - **`ProviderSerializationContractTest`** (13 tests) : gating RIB/scalaires par omission, réfs compta en objet `{id,code,label}`, `isArchived`, embed categories/sites liste+détail, sous-collections, enveloppe AP4 ; `testDodReferenceJsonShape` dumpe le JSON réel (`PROVIDER_DOD_DUMP=1`). - **`ProviderAuditTest`** (5 tests) : create/update/archive (`technique.Provider`), iban/bic dans le diff (`technique.ProviderRib`, pas dAuditIgnore), trace M2M `sites`. - **`ProviderListTest`** étendu : `?pagination=false`, anti-N+1, filtre `?typeCode=PRESTATAIRE`. - **`ProviderRbacGatingTest`** étendu : restauration en conflit de nom → 409 (RG-3.14). - **`ProviderFixtures`** (§ 8.4) : démo idempotente (complet VIREMENT+banque+RIB, LCR+RIB, CHEQUE multi-cat, minimal, archivé) répartie sur sites 86/17/82 ; skip en env `test`. - Helper `seedCompleteProvider` ; spec § 4.0.bis : gabarits remplacés par les captures réelles (liste + détail avec/sans accounting.view). ## Vérifications - `make php-cs-fixer-allow-risky` → 0 fichier - `make test` → OK, 677 tests, 3328 assertions (garde-fous globaux verts) ## Notes - MR stackée sur ERP-138 (base = sa branche). - Fixtures démo exercées en dev via `make fixtures` (autowiring vérifié). --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #100
177 lines
7.2 KiB
PHP
177 lines
7.2 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());
|
|
}
|
|
|
|
public function testRestoreWithNameConflictReturns409(): void
|
|
{
|
|
// Un prestataire archive porte un nom qu'un prestataire ACTIF a repris
|
|
// entre-temps (autorise par l'index partiel : l'archive n'y figure pas).
|
|
$archived = $this->seedProvider('Conflit Co', [self::SITE_86], isArchived: true);
|
|
$this->seedProvider('Conflit Co', [self::SITE_86]); // actif, meme nom normalise
|
|
|
|
$client = $this->createAdminClient();
|
|
$response = $client->request('PATCH', '/api/providers/'.$archived->getId(), [
|
|
'headers' => ['Content-Type' => self::MERGE],
|
|
'json' => ['isArchived' => false],
|
|
]);
|
|
|
|
// RG-3.14 : restaurer ferait deux actifs homonymes -> 409 (pas de 500 SQL).
|
|
self::assertSame(409, $response->getStatusCode());
|
|
}
|
|
}
|