|
|
@@ -73,4 +73,84 @@ final class ConsumersControllerTest extends AppTestCase
|
|
|
self::assertSame(200, $patch->getStatusCode());
|
|
|
self::assertFalse($this->decode($patch)['audit_enabled']);
|
|
|
}
|
|
|
+
|
|
|
+ public function testAuditEnabledToggleEmitsDedicatedAuditRow(): void
|
|
|
+ {
|
|
|
+ // SEC_REVIEW F41: an admin flipping `audit_enabled` for a consumer
|
|
|
+ // must leave a flat alertable trail SOC tooling can match on with
|
|
|
+ // `action = 'consumer.audit_toggled'` — without walking into the
|
|
|
+ // metadata `changes` blob of the standard `consumer.updated` row.
|
|
|
+ $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
|
|
|
+ $policyId = (int) $this->db->fetchOne(
|
|
|
+ 'SELECT id FROM policies WHERE name = :name',
|
|
|
+ ['name' => 'moderate'],
|
|
|
+ );
|
|
|
+
|
|
|
+ $created = $this->request(
|
|
|
+ 'POST',
|
|
|
+ '/api/v1/admin/consumers',
|
|
|
+ ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
|
|
|
+ json_encode(['name' => 'fw-audit-signal', 'policy_id' => $policyId]) ?: null,
|
|
|
+ );
|
|
|
+ $consumerId = (int) $this->decode($created)['id'];
|
|
|
+
|
|
|
+ $this->request(
|
|
|
+ 'PATCH',
|
|
|
+ "/api/v1/admin/consumers/{$consumerId}",
|
|
|
+ ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
|
|
|
+ json_encode(['audit_enabled' => false]) ?: null,
|
|
|
+ );
|
|
|
+
|
|
|
+ $rows = $this->db->fetchAllAssociative(
|
|
|
+ "SELECT action, details_json FROM audit_log WHERE target_type = 'consumer' AND target_id = ? ORDER BY id",
|
|
|
+ [(string) $consumerId],
|
|
|
+ );
|
|
|
+ $actions = array_column($rows, 'action');
|
|
|
+ self::assertContains('consumer.updated', $actions);
|
|
|
+ self::assertContains('consumer.audit_toggled', $actions);
|
|
|
+
|
|
|
+ $toggleRow = null;
|
|
|
+ foreach ($rows as $row) {
|
|
|
+ if ($row['action'] === 'consumer.audit_toggled') {
|
|
|
+ $toggleRow = $row;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ self::assertNotNull($toggleRow);
|
|
|
+ $meta = json_decode((string) $toggleRow['details_json'], true);
|
|
|
+ self::assertSame(true, $meta['from'] ?? null);
|
|
|
+ self::assertSame(false, $meta['to'] ?? null);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testAuditEnabledNoOpDoesNotEmitDedicatedRow(): void
|
|
|
+ {
|
|
|
+ $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
|
|
|
+ $policyId = (int) $this->db->fetchOne(
|
|
|
+ 'SELECT id FROM policies WHERE name = :name',
|
|
|
+ ['name' => 'moderate'],
|
|
|
+ );
|
|
|
+
|
|
|
+ $created = $this->request(
|
|
|
+ 'POST',
|
|
|
+ '/api/v1/admin/consumers',
|
|
|
+ ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
|
|
|
+ json_encode(['name' => 'fw-noop', 'policy_id' => $policyId]) ?: null,
|
|
|
+ );
|
|
|
+ $consumerId = (int) $this->decode($created)['id'];
|
|
|
+
|
|
|
+ // PATCH `audit_enabled` to its current value (no-op) — must NOT
|
|
|
+ // fire the toggle signal.
|
|
|
+ $this->request(
|
|
|
+ 'PATCH',
|
|
|
+ "/api/v1/admin/consumers/{$consumerId}",
|
|
|
+ ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
|
|
|
+ json_encode(['audit_enabled' => true]) ?: null,
|
|
|
+ );
|
|
|
+
|
|
|
+ $actions = $this->db->fetchFirstColumn(
|
|
|
+ "SELECT action FROM audit_log WHERE target_type = 'consumer' AND target_id = ?",
|
|
|
+ [(string) $consumerId],
|
|
|
+ );
|
|
|
+ self::assertNotContains('consumer.audit_toggled', $actions);
|
|
|
+ }
|
|
|
}
|