|
|
@@ -153,4 +153,111 @@ final class UserRepositoryTest extends TestCase
|
|
|
$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');
|
|
|
+ }
|
|
|
}
|