1
0

ConsumersControllerTest.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Integration\Admin;
  4. use App\Domain\Auth\Role;
  5. use App\Domain\Auth\TokenKind;
  6. use App\Tests\Integration\Support\AppTestCase;
  7. final class ConsumersControllerTest extends AppTestCase
  8. {
  9. public function testCreateAndListConsumer(): void
  10. {
  11. $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
  12. $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :name', ['name' => 'moderate']);
  13. $created = $this->request(
  14. 'POST',
  15. '/api/v1/admin/consumers',
  16. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  17. json_encode(['name' => 'fw-edge-01', 'policy_id' => $policyId]) ?: null,
  18. );
  19. self::assertSame(201, $created->getStatusCode());
  20. $body = $this->decode($created);
  21. self::assertSame('fw-edge-01', $body['name']);
  22. self::assertSame($policyId, $body['policy_id']);
  23. $list = $this->request('GET', '/api/v1/admin/consumers', [
  24. 'Authorization' => 'Bearer ' . $token,
  25. ]);
  26. self::assertSame(200, $list->getStatusCode());
  27. $listBody = $this->decode($list);
  28. self::assertGreaterThan(0, $listBody['total']);
  29. }
  30. public function testCreateRejectsUnknownPolicy(): void
  31. {
  32. $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
  33. $resp = $this->request(
  34. 'POST',
  35. '/api/v1/admin/consumers',
  36. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  37. json_encode(['name' => 'bogus', 'policy_id' => 99999]) ?: null,
  38. );
  39. self::assertSame(400, $resp->getStatusCode());
  40. self::assertArrayHasKey('policy_id', $this->decode($resp)['details']);
  41. }
  42. public function testPatchTogglesAuditEnabled(): void
  43. {
  44. $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
  45. $policyId = (int) $this->db->fetchOne('SELECT id FROM policies WHERE name = :name', ['name' => 'moderate']);
  46. $created = $this->request(
  47. 'POST',
  48. '/api/v1/admin/consumers',
  49. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  50. json_encode(['name' => 'fw-audit-toggle', 'policy_id' => $policyId]) ?: null,
  51. );
  52. self::assertSame(201, $created->getStatusCode());
  53. $body = $this->decode($created);
  54. self::assertTrue($body['audit_enabled']);
  55. $consumerId = (int) $body['id'];
  56. $patch = $this->request(
  57. 'PATCH',
  58. "/api/v1/admin/consumers/{$consumerId}",
  59. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  60. json_encode(['audit_enabled' => false]) ?: null,
  61. );
  62. self::assertSame(200, $patch->getStatusCode());
  63. self::assertFalse($this->decode($patch)['audit_enabled']);
  64. }
  65. public function testAuditEnabledToggleEmitsDedicatedAuditRow(): void
  66. {
  67. // SEC_REVIEW F41: an admin flipping `audit_enabled` for a consumer
  68. // must leave a flat alertable trail SOC tooling can match on with
  69. // `action = 'consumer.audit_toggled'` — without walking into the
  70. // metadata `changes` blob of the standard `consumer.updated` row.
  71. $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
  72. $policyId = (int) $this->db->fetchOne(
  73. 'SELECT id FROM policies WHERE name = :name',
  74. ['name' => 'moderate'],
  75. );
  76. $created = $this->request(
  77. 'POST',
  78. '/api/v1/admin/consumers',
  79. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  80. json_encode(['name' => 'fw-audit-signal', 'policy_id' => $policyId]) ?: null,
  81. );
  82. $consumerId = (int) $this->decode($created)['id'];
  83. $this->request(
  84. 'PATCH',
  85. "/api/v1/admin/consumers/{$consumerId}",
  86. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  87. json_encode(['audit_enabled' => false]) ?: null,
  88. );
  89. $rows = $this->db->fetchAllAssociative(
  90. "SELECT action, details_json FROM audit_log WHERE target_type = 'consumer' AND target_id = ? ORDER BY id",
  91. [(string) $consumerId],
  92. );
  93. $actions = array_column($rows, 'action');
  94. self::assertContains('consumer.updated', $actions);
  95. self::assertContains('consumer.audit_toggled', $actions);
  96. $toggleRow = null;
  97. foreach ($rows as $row) {
  98. if ($row['action'] === 'consumer.audit_toggled') {
  99. $toggleRow = $row;
  100. break;
  101. }
  102. }
  103. self::assertNotNull($toggleRow);
  104. $meta = json_decode((string) $toggleRow['details_json'], true);
  105. self::assertSame(true, $meta['from'] ?? null);
  106. self::assertSame(false, $meta['to'] ?? null);
  107. }
  108. public function testAuditEnabledNoOpDoesNotEmitDedicatedRow(): void
  109. {
  110. $token = $this->createToken(TokenKind::Admin, role: Role::Admin);
  111. $policyId = (int) $this->db->fetchOne(
  112. 'SELECT id FROM policies WHERE name = :name',
  113. ['name' => 'moderate'],
  114. );
  115. $created = $this->request(
  116. 'POST',
  117. '/api/v1/admin/consumers',
  118. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  119. json_encode(['name' => 'fw-noop', 'policy_id' => $policyId]) ?: null,
  120. );
  121. $consumerId = (int) $this->decode($created)['id'];
  122. // PATCH `audit_enabled` to its current value (no-op) — must NOT
  123. // fire the toggle signal.
  124. $this->request(
  125. 'PATCH',
  126. "/api/v1/admin/consumers/{$consumerId}",
  127. ['Authorization' => 'Bearer ' . $token, 'Content-Type' => 'application/json'],
  128. json_encode(['audit_enabled' => true]) ?: null,
  129. );
  130. $actions = $this->db->fetchFirstColumn(
  131. "SELECT action FROM audit_log WHERE target_type = 'consumer' AND target_id = ?",
  132. [(string) $consumerId],
  133. );
  134. self::assertNotContains('consumer.audit_toggled', $actions);
  135. }
  136. }