clock = new class implements ClockInterface { public function now(): \DateTimeImmutable { return new \DateTimeImmutable(); } }; } protected function tearDown(): void { $this->clock = null; } public function testGenerateAndVerify(): void { $totpService = new TOTPService(); $secret = $totpService->generateSecret(); // Generate a current OTP using the secret and clock $otp = TOTP::create($secret, clock: $this->clock)->now(); $this->assertTrue($totpService->verify($secret, $otp)); } public function testGenerateSecretUniqueness(): void { $totpService = new TOTPService(); $secret1 = $totpService->generateSecret(); $secret2 = $totpService->generateSecret(); $this->assertNotEquals($secret1, $secret2); $this->assertNotEmpty($secret1); $this->assertNotEmpty($secret2); } public function testVerifyWithWrongOTP(): void { $totpService = new TOTPService(); $secret = $totpService->generateSecret(); // correct OTP $otpCorrect = TOTP::create($secret, clock: $this->clock)->now(); $this->assertTrue($totpService->verify($secret, $otpCorrect)); // wrong OTP $wrongOtp = '000000'; $this->assertFalse($totpService->verify($secret, $wrongOtp)); } public function testGetProvisioningUri(): void { $totpService = new TOTPService(); $secret = $totpService->generateSecret(); // Use a known account name and issuer $accountName = 'alice@example.com'; $issuer = 'SiteWeaverCMS'; $uri = $totpService->getProvisioningUri($accountName, $issuer); // The URI should start with otpauth://totp/ $this->assertStringStartsWith('otpauth://totp/', $uri); // It should contain the issuer and account name (URL‑encoded) $expectedIssuer = rawurlencode($issuer); $expectedAccount = rawurlencode($accountName); $this->assertStringContainsString($expectedIssuer, $uri); $this->assertStringContainsString($expectedAccount, $uri); // Compare with the library's own provisioning URI $libUri = TOTP::create($secret, clock: $this->clock) ->withLabel($accountName) ->withIssuer($issuer) ->getProvisioningUri(); $this->assertEquals($libUri, $uri); } }