UserControllerTest.php 4.1 KB

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