| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263 |
- <?php
- declare(strict_types=1);
- namespace App\Tests\Repositories;
- use App\Repositories\UserRepository;
- use App\Tests\TestCase;
- /**
- * Covers the upsert + admin-promotion contract used by the auth flows.
- * Caller-side gating (R01-N03's BOOTSTRAP_ADMIN_* env-bootstrap) lives in
- * `AuthController` + `BootstrapAdmin`; this suite only pins the repo's
- * mechanical promoteToAdmin / forceAdmin behaviour.
- */
- final class UserRepositoryTest extends TestCase
- {
- public function testFirstUserBecomesAdminWhenPromoted(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $this->assertSame(0, $users->count());
- $r = $users->upsertFromOidc(
- oid: 'oid-alice',
- email: 'alice@example.com',
- name: 'Alice',
- promoteToAdmin: true, // caller's decision — see BootstrapAdmin::matches
- );
- $this->assertTrue($r['user']->isAdmin);
- $this->assertNull($r['before']);
- }
- public function testSecondUserDoesNotBecomeAdmin(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('oid-alice', 'alice@x', 'Alice', true);
- // Caller passes promoteToAdmin=false because an admin already exists.
- $r2 = $users->upsertFromOidc('oid-bob', 'bob@x', 'Bob', false);
- $this->assertFalse($r2['user']->isAdmin);
- }
- public function testReLoginDoesNotRegressAdminStatus(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('oid-alice', 'alice@x', 'Alice', true);
- // simulate a later login: count > 0 so promoteToAdmin=false
- $r = $users->upsertFromOidc('oid-alice', 'alice@x', 'Alice', false);
- $this->assertTrue($r['user']->isAdmin, 're-login must not demote admin');
- $this->assertNotNull($r['before']);
- }
- public function testForceAdminPromotesEvenOnUpdate(): void
- {
- // Local-admin login path sets forceAdmin=true so a demoted user gets
- // promoted back on next sign-in.
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $r1 = $users->upsertFromOidc('local:admin@x', 'admin@x', 'Admin', true, true);
- $this->assertTrue($r1['user']->isAdmin);
- // Manually demote.
- $pdo->exec('UPDATE users SET is_admin = 0 WHERE id = ' . $r1['user']->id);
- $r2 = $users->upsertFromOidc('local:admin@x', 'admin@x', 'Admin', false, true);
- $this->assertTrue($r2['user']->isAdmin, 'forceAdmin must re-promote on update');
- }
- public function testUpsertUpdatesEmailAndName(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('oid-alice', 'old@x', 'Old Name', true);
- $r = $users->upsertFromOidc('oid-alice', 'new@x', 'New Name', false);
- $this->assertSame('new@x', $r['user']->email);
- $this->assertSame('New Name', $r['user']->displayName);
- }
- public function testCountReflectsInsertedUsers(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $this->assertSame(0, $users->count());
- $users->upsertFromOidc('oid-1', 'a@x', 'A', true);
- $this->assertSame(1, $users->count());
- $users->upsertFromOidc('oid-2', 'b@x', 'B', false);
- $this->assertSame(2, $users->count());
- // Re-upsert existing user shouldn't add a row.
- $users->upsertFromOidc('oid-1', 'a@x', 'A', false);
- $this->assertSame(2, $users->count());
- }
- // ------------------------------------------------------------------
- // Phase 9: users management page helpers
- // ------------------------------------------------------------------
- public function testAllReturnsEveryUserOrderedByEmail(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('oid-c', 'carol@x', 'Carol', true);
- $users->upsertFromOidc('oid-a', 'alice@x', 'Alice', false);
- $users->upsertFromOidc('oid-b', 'BOB@x', 'Bob', false);
- $all = $users->all();
- $this->assertCount(3, $all);
- $this->assertSame(['alice@x', 'BOB@x', 'carol@x'], array_map(fn($u) => $u->email, $all));
- }
- public function testCountAdmins(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $this->assertSame(0, $users->countAdmins());
- $users->upsertFromOidc('oid-a', 'a@x', 'A', true);
- $this->assertSame(1, $users->countAdmins());
- $users->upsertFromOidc('oid-b', 'b@x', 'B', false);
- $this->assertSame(1, $users->countAdmins());
- $users->upsertFromOidc('oid-c', 'c@x', 'C', false, true); // forceAdmin
- $this->assertSame(2, $users->countAdmins());
- }
- public function testSetAdminTogglesAndReportsDiff(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('oid-a', 'a@x', 'A', true);
- $users->upsertFromOidc('oid-b', 'b@x', 'B', false);
- $bob = $users->findByOid('oid-b');
- $r = $users->setAdmin($bob->id, true);
- $this->assertFalse($r['before']->isAdmin);
- $this->assertTrue ($r['after']->isAdmin);
- $this->assertSame(2, $users->countAdmins());
- $r = $users->setAdmin($bob->id, false);
- $this->assertTrue ($r['before']->isAdmin);
- $this->assertFalse($r['after']->isAdmin);
- $this->assertSame(1, $users->countAdmins());
- }
- // ------------------------------------------------------------------
- // R01-N23: tombstone (soft-delete for privacy)
- // ------------------------------------------------------------------
- public function testFreshUserIsNotTombstoned(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $r = $users->upsertFromOidc('oid-a', 'a@x', 'Alice', true);
- $this->assertFalse($r['user']->isTombstoned());
- $this->assertNull($r['user']->tombstonedAt);
- }
- public function testSetTombstonedStampsAndForcesDemote(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('oid-a', 'a@x', 'A', true); // first admin
- $users->upsertFromOidc('oid-b', 'b@x', 'B', false);
- $users->upsertFromOidc('oid-c', 'c@x', 'C', false, true); // forceAdmin → second admin
- $bob = $users->findByOid('oid-b');
- $r = $users->setTombstoned($bob->id, true);
- $this->assertFalse($r['before']->isTombstoned());
- $this->assertTrue ($r['after']->isTombstoned());
- $this->assertNotNull($r['after']->tombstonedAt);
- $this->assertSame('(former user)', $r['after']->publicEmail());
- $this->assertSame('(former user)', $r['after']->publicDisplayName());
- $this->assertSame('b@x', $r['after']->email, 'audit-snapshot email is preserved');
- $this->assertSame('B', $r['after']->displayName, 'audit-snapshot display name is preserved');
- $this->assertFalse($r['after']->isAdmin, 'tombstone forces is_admin=0');
- }
- public function testSetTombstonedClearsAdminEvenForCurrentlyAdminUsers(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('oid-a', 'a@x', 'A', true); // first admin (kept)
- $users->upsertFromOidc('oid-b', 'b@x', 'B', false, true); // forceAdmin → second admin
- $bob = $users->findByOid('oid-b');
- $this->assertTrue($bob->isAdmin);
- $users->setTombstoned($bob->id, true);
- $bob = $users->findByOid('oid-b');
- $this->assertFalse($bob->isAdmin, 'tombstoning an admin must demote them');
- $this->assertSame(1, $users->countAdmins());
- }
- public function testRestoreClearsTombstoneButDoesNotPromote(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('oid-a', 'a@x', 'A', true);
- $users->upsertFromOidc('oid-b', 'b@x', 'B', false, true);
- $bob = $users->findByOid('oid-b');
- $users->setTombstoned($bob->id, true);
- $bob = $users->findByOid('oid-b');
- $this->assertTrue($bob->isTombstoned());
- $this->assertFalse($bob->isAdmin, 'precondition: tombstone demoted bob');
- $r = $users->setTombstoned($bob->id, false);
- $this->assertFalse($r['after']->isTombstoned());
- $this->assertNull($r['after']->tombstonedAt);
- $this->assertFalse($r['after']->isAdmin, 'restore must NOT auto-promote');
- }
- public function testOidcSignInUntombstones(): void
- {
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('oid-a', 'a@x', 'A', true);
- $alice = $users->findByOid('oid-a');
- $users->setTombstoned($alice->id, true);
- $this->assertTrue($users->findByOid('oid-a')->isTombstoned());
- // A successful OIDC sign-in (the upsert path the AuthController calls)
- // must clear the tombstone — the user has demonstrated they still hold
- // the identity, so the `(former user)` label would lie.
- $r = $users->upsertFromOidc('oid-a', 'a@x', 'A', false);
- $this->assertFalse($r['user']->isTombstoned());
- }
- public function testForceAdminAlsoUntombstones(): void
- {
- // Local-admin login path: forceAdmin=true also clears any
- // tombstone, so a tombstoned configured admin can come back.
- $pdo = $this->makeDb();
- $users = new UserRepository($pdo);
- $users->upsertFromOidc('local:admin@x', 'admin@x', 'Admin', true, true);
- $admin = $users->findByOid('local:admin@x');
- $users->setTombstoned($admin->id, true);
- $r = $users->upsertFromOidc('local:admin@x', 'admin@x', 'Admin', false, true);
- $this->assertFalse($r['user']->isTombstoned());
- $this->assertTrue($r['user']->isAdmin, 'forceAdmin path still re-promotes');
- }
- }
|