setConfig($this->makeConfig()); $manager->setLogger(new NullLogger()); return $manager; } public function testGenerateReturnsRequestedCount(): void { $manager = $this->makeManager(); $codes = $manager->generate(5); self::assertCount(5, $codes); foreach ($codes as $code) { self::assertIsString($code); // codes are lowercase alphanumeric self::assertMatchesRegularExpression('/^[a-z0-9]+$/', $code); } } public function testGenerateDefaultCount(): void { $manager = $this->makeManager(); $codes = $manager->generate(); self::assertCount(10, $codes); } public function testGenerateZeroReturnsEmptyArray(): void { $manager = $this->makeManager(); $codes = $manager->generate(0); self::assertSame([], $codes); } public function testGeneratedCodesAreStoredInCache(): void { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); $codes = $manager->generate(3); // each code should be stored as a backup_ key foreach ($codes as $code) { $key = 'backup_' . strtolower($code); // the manager uses makeCacheKey which sanitizes, but for alphanumeric it's identity $item = $pool->getItem($key); self::assertTrue($item->isHit(), "Expected cache hit for key: $key"); self::assertTrue($item->get(), "Expected code to be marked valid (true)"); } } public function testGeneratedCodesHaveFarFutureExpiry(): void { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); $codes = $manager->generate(1); $code = $codes[0]; $item = $pool->getItem('backup_' . strtolower($code)); $expiry = $item->getMetadata()['expiry']; self::assertGreaterThan((new \DateTimeImmutable('+10 years'))->getTimestamp(), (int) $expiry); } public function testVerifyAndConsumeValidCode(): void { $manager = $this->makeManager(); $codes = $manager->generate(2); $code = $codes[0]; self::assertTrue($manager->verifyAndConsume($code)); } public function testVerifyAndConsumeMarksCodeAsUsed(): void { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); $codes = $manager->generate(1); $code = $codes[0]; // first use succeeds self::assertTrue($manager->verifyAndConsume($code)); // second use fails (already consumed) self::assertFalse($manager->verifyAndConsume($code)); } public function testVerifyAndConsumeInvalidCode(): void { $manager = $this->makeManager(); self::assertFalse($manager->verifyAndConsume('nonexistent_code')); } public function testVerifyAndConsumeIsCaseInsensitive(): void { $manager = $this->makeManager(); $codes = $manager->generate(1); $code = $codes[0]; // uppercase version should still work self::assertTrue($manager->verifyAndConsume(strtoupper($code))); } public function testVerifyAndConsumeStripsInvalidCharacters(): void { $manager = $this->makeManager(); $codes = $manager->generate(1); $code = $codes[0]; // inject spaces and special chars — should be stripped self::assertTrue($manager->verifyAndConsume(' ' . $code . '!!')); } public function testExpireRemovesAllBackupCodes(): void { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); $codes = $manager->generate(5); $manager->expire(); // all backup keys should be gone foreach ($codes as $code) { self::assertFalse($pool->hasItem('backup_' . strtolower($code))); } } public function testExpireWhenNoBackupCodesIsNoop(): void { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); // should not throw $manager->expire(); // this passes if no exception was thrown self::assertTrue(true); } public function testExpireRemovesOnlyBackupPrefixedKeys(): void { $pool = new ArrayAdapter(); $manager = $this->makeManager($pool); $codes = $manager->generate(3); // add a non-backup key $item = $pool->getItem('cookie_session'); $item->set('data'); $pool->save($item); $manager->expire(); // non-backup key survives self::assertTrue($pool->hasItem('cookie_session')); // backup keys are gone foreach ($codes as $code) { self::assertFalse($pool->hasItem('backup_' . strtolower($code))); } } }