get('doctrine.dbal.audit_connection'); $this->auditConnection = $conn; } protected function tearDown(): void { if (null !== $this->auditConnection) { $this->auditConnection->close(); } parent::tearDown(); } /** * RG-1.27 : createdAt / createdBy sont poses au POST puis figes ; updatedBy * suit l'auteur de la derniere modification. On cree en admin puis on * modifie avec un user `commercial.clients.manage` distinct : createdBy reste * l'admin, updatedBy devient le manager, createdAt ne bouge pas. */ public function testCreatedFrozenAndUpdatedByReflectsModifier(): void { // 1. User modificateur (non-admin) cree AVANT le reboot de kernel induit // par les clients authentifies suivants ; il est persiste en base. $manageCreds = $this->createUserWithPermission('commercial.clients.manage'); // 2. Creation en admin (createdBy = admin). $admin = $this->createAdminClient(); $cat = $this->createCategory('SECTEUR'); $created = $admin->request('POST', '/api/clients', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'companyName' => 'Blamable Co', 'firstName' => 'A', 'phonePrimary' => '0102030405', 'email' => 'blamable@test.fr', 'categories' => ['/api/categories/'.$cat->getId()], ], ])->toArray(); self::assertResponseStatusCodeSame(201); $id = (int) $created['id']; $createdAtTs = new DateTimeImmutable((string) $created['createdAt'])->getTimestamp(); // 3. Modification par le manager (updatedBy = manager). $manage = $this->authenticatedClient($manageCreds['username'], $manageCreds['password']); $manage->request('PATCH', '/api/clients/'.$id, [ 'headers' => ['Content-Type' => self::MERGE], 'json' => ['companyName' => 'Blamable Renamed'], ]); self::assertResponseStatusCodeSame(200); // 4. Verification cote base (etat re-charge depuis la BDD). $em = $this->getEm(); $em->clear(); $reloaded = $em->getRepository(ClientEntity::class)->find($id); self::assertNotNull($reloaded); self::assertSame('admin', $reloaded->getCreatedBy()?->getUserIdentifier(), 'createdBy doit rester l\'admin createur.'); self::assertSame( $manageCreds['username'], $reloaded->getUpdatedBy()?->getUserIdentifier(), 'updatedBy doit refleter le dernier modificateur.', ); self::assertSame($createdAtTs, $reloaded->getCreatedAt()?->getTimestamp(), 'createdAt doit etre fige au POST.'); self::assertNotNull($reloaded->getUpdatedAt()); self::assertGreaterThanOrEqual($createdAtTs, $reloaded->getUpdatedAt()->getTimestamp()); } /** * Audit § 6.1 : la creation d'un RIB produit une ligne audit_log * `commercial.ClientRib` / `create` dont le snapshot contient iban et bic * (champs volontairement NON ignores). */ public function testRibCreateAuditIncludesIbanAndBic(): void { $admin = $this->createAdminClient(); $seed = $this->seedClient('Rib Audit Host'); $rib = $admin->request('POST', '/api/clients/'.$seed->getId().'/ribs', [ 'headers' => ['Content-Type' => self::LD], 'json' => [ 'label' => 'Compte audite', 'bic' => self::VALID_BIC, 'iban' => self::VALID_IBAN, ], ])->toArray(); self::assertResponseStatusCodeSame(201); $rows = $this->auditConnection->fetchAllAssociative( 'SELECT changes FROM audit_log ' .'WHERE entity_type = :type AND entity_id = :id AND action = :action ' .'ORDER BY performed_at DESC', ['type' => self::RIB_TYPE, 'id' => (string) $rib['id'], 'action' => 'create'], ); self::assertGreaterThanOrEqual(1, count($rows), 'Un audit_log "create" doit etre genere pour le RIB.'); /** @var array $changes */ $changes = json_decode((string) $rows[0]['changes'], true, flags: JSON_THROW_ON_ERROR); self::assertArrayHasKey('iban', $changes, 'iban doit figurer dans le diff audite (pas d\'AuditIgnore).'); self::assertArrayHasKey('bic', $changes, 'bic doit figurer dans le diff audite (pas d\'AuditIgnore).'); self::assertSame(self::VALID_IBAN, $changes['iban']); self::assertSame(self::VALID_BIC, $changes['bic']); } }