1
0

RoleMappingRepositoryTest.php 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Integration\Auth;
  4. use App\Domain\Auth\Role;
  5. use App\Infrastructure\Auth\RoleMappingRepository;
  6. use App\Tests\Integration\Support\AppTestCase;
  7. /**
  8. * SEC_REVIEW F51 — `RoleMappingRepository::resolveRole` builds an IN
  9. * clause from `count($groupIds)` placeholders. The PHPDoc declares
  10. * `list<string>` but PHP doesn't enforce that at runtime; a future
  11. * caller could pass a mixed array and break the placeholder/bind
  12. * invariant. The repository now `array_filter`s to strings and
  13. * re-keys with `array_values` before generating placeholders.
  14. */
  15. final class RoleMappingRepositoryTest extends AppTestCase
  16. {
  17. public function testResolvesHighestRoleAcrossMatchingGroups(): void
  18. {
  19. $this->seedMapping('group-viewer', Role::Viewer);
  20. $this->seedMapping('group-admin', Role::Admin);
  21. $repo = new RoleMappingRepository($this->db);
  22. self::assertSame(
  23. Role::Admin,
  24. $repo->resolveRole(['group-viewer', 'group-admin'], Role::Viewer),
  25. );
  26. }
  27. public function testFallsBackToDefaultWhenNoGroupMatches(): void
  28. {
  29. $repo = new RoleMappingRepository($this->db);
  30. self::assertSame(
  31. Role::Viewer,
  32. $repo->resolveRole(['unmapped-group'], Role::Viewer),
  33. );
  34. }
  35. public function testEmptyListReturnsDefault(): void
  36. {
  37. $repo = new RoleMappingRepository($this->db);
  38. self::assertSame(Role::Viewer, $repo->resolveRole([], Role::Viewer));
  39. }
  40. public function testNonStringEntriesAreFilteredOut(): void
  41. {
  42. // SEC_REVIEW F51: a mixed array (e.g. PHPDoc lied, callsite
  43. // pushed an int / null / bool by mistake) must not crash the
  44. // placeholder math or change the result. The non-string
  45. // entries are discarded; the remaining strings drive the
  46. // lookup.
  47. $this->seedMapping('group-admin', Role::Admin);
  48. $repo = new RoleMappingRepository($this->db);
  49. /** @var list<string> $groups @phpstan-ignore-line — exercising the runtime guard */
  50. $groups = ['group-admin', 42, null, true, ['nested']];
  51. self::assertSame(
  52. Role::Admin,
  53. $repo->resolveRole($groups, Role::Viewer),
  54. );
  55. }
  56. public function testAllNonStringEntriesFallsBackToDefault(): void
  57. {
  58. // After filtering, the list is empty — caller meant something
  59. // but typed nothing usable; treat as "no groups → default".
  60. $repo = new RoleMappingRepository($this->db);
  61. /** @var list<string> $groups @phpstan-ignore-line — exercising the runtime guard */
  62. $groups = [42, null, true, ['nested']];
  63. self::assertSame(
  64. Role::Viewer,
  65. $repo->resolveRole($groups, Role::Viewer),
  66. );
  67. }
  68. public function testHashWithGapsInIndicesIsAccepted(): void
  69. {
  70. // Another PHPDoc-lies case: caller passed a hash whose keys
  71. // aren't 0..n-1. `array_values(array_filter(...))` re-keys
  72. // before the placeholder/bind invariant is built.
  73. $this->seedMapping('group-operator', Role::Operator);
  74. $repo = new RoleMappingRepository($this->db);
  75. /** @var list<string> $groups @phpstan-ignore-line — exercising the runtime guard */
  76. $groups = [5 => 'group-operator', 12 => 'unmapped'];
  77. self::assertSame(
  78. Role::Operator,
  79. $repo->resolveRole($groups, Role::Viewer),
  80. );
  81. }
  82. private function seedMapping(string $groupId, Role $role): void
  83. {
  84. $this->db->insert('oidc_role_mappings', [
  85. 'group_id' => $groupId,
  86. 'role' => $role->value,
  87. 'created_at' => (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s'),
  88. ]);
  89. }
  90. }