UserControllerTest.php 3.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Controllers;
  4. use App\Controllers\UserController;
  5. use PHPUnit\Framework\Attributes\DataProvider;
  6. use PHPUnit\Framework\TestCase;
  7. /**
  8. * The demotion guardrails are extracted into a pure static method; this
  9. * test pins the rule matrix without any SessionGuard / PDO setup.
  10. */
  11. final class UserControllerTest extends TestCase
  12. {
  13. /** @return list<array{int,int,bool,bool,int,?string,string}> */
  14. public static function guardrailCases(): array
  15. {
  16. // [actorId, targetId, wasAdmin, willBeAdmin, totalAdmins, expected, label]
  17. return [
  18. [1, 1, true, false, 2, 'self_demote', 'self demote, 2 admins'],
  19. [1, 1, true, false, 1, 'self_demote', 'self demote, only admin — self wins priority'],
  20. [1, 2, true, false, 1, 'last_admin', 'demote the only admin (different user)'],
  21. [1, 2, true, false, 2, null, 'demote someone else when another admin remains'],
  22. [1, 2, false, true, 1, null, 'promote — never blocked'],
  23. [1, 2, true, true, 1, null, 'no-op — never blocked'],
  24. [1, 2, false, false, 1, null, 'no-op on non-admin — never blocked'],
  25. [1, 1, false, true, 1, null, 'actor promoting themselves? odd but not blocked'],
  26. ];
  27. }
  28. #[DataProvider('guardrailCases')]
  29. public function testDemoteGuardrail(
  30. int $actor,
  31. int $target,
  32. bool $was,
  33. bool $will,
  34. int $total,
  35. ?string $expected,
  36. string $label,
  37. ): void {
  38. $this->assertSame(
  39. $expected,
  40. UserController::demoteGuardrail($actor, $target, $was, $will, $total),
  41. $label,
  42. );
  43. }
  44. /** @return list<array{int,int,bool,bool,bool,int,?string,string}> */
  45. public static function tombstoneCases(): array
  46. {
  47. // [actorId, targetId, isTombstoned, isAdmin, tombstone, totalAdmins, expected, label]
  48. return [
  49. // self
  50. [1, 1, false, false, true, 2, 'self_tombstone', 'tombstoning self, even non-admin'],
  51. [1, 1, true, false, false, 2, null, 'restoring self (whoever did the original tombstone is gone) — allowed'],
  52. // last admin guard kicks in only on tombstone, not restore
  53. [1, 2, false, true, true, 1, 'last_admin', 'tombstoning the only remaining admin'],
  54. [1, 2, false, true, true, 2, null, 'tombstoning an admin when another remains'],
  55. [1, 2, false, false, true, 1, null, 'tombstoning a non-admin when target is the actor pool — allowed'],
  56. // restore never trips last_admin (it merely clears the marker; admin status was already 0)
  57. [1, 2, true, false, false, 1, null, 'restore is always allowed even with one admin'],
  58. // noop short-circuits before any other rule
  59. [1, 1, true, false, true, 1, 'noop', 'tombstone of already-tombstoned (and self) → noop wins'],
  60. [1, 2, false, false, false, 1, 'noop', 'restore of already-active → noop'],
  61. ];
  62. }
  63. #[DataProvider('tombstoneCases')]
  64. public function testTombstoneGuardrail(
  65. int $actor,
  66. int $target,
  67. bool $isTombstoned,
  68. bool $isAdmin,
  69. bool $tombstone,
  70. int $totalAdmins,
  71. ?string $expected,
  72. string $label,
  73. ): void {
  74. $this->assertSame(
  75. $expected,
  76. UserController::tombstoneGuardrail(
  77. actorId: $actor,
  78. targetId: $target,
  79. targetIsTombstoned: $isTombstoned,
  80. targetIsAdmin: $isAdmin,
  81. tombstone: $tombstone,
  82. totalAdmins: $totalAdmins,
  83. ),
  84. $label,
  85. );
  86. }
  87. }