userId); self::assertSame($tenantId, $token->tenantId); self::assertTrue($token->deviceFingerprint->equals($fingerprint)); self::assertEquals($issuedAt, $token->issuedAt); self::assertNull($token->rotatedFrom); self::assertFalse($token->isRotated); } #[Test] public function createSetsExpirationBasedOnTtl(): void { $issuedAt = new DateTimeImmutable('2026-01-31 10:00:00'); $ttl = 86400; // 1 day $token = RefreshToken::create( UserId::generate(), TenantId::fromString(self::TENANT_ID), DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'), $issuedAt, $ttl, ); $expectedExpiry = $issuedAt->modify('+86400 seconds'); self::assertEquals($expectedExpiry, $token->expiresAt); } #[Test] public function rotateCreatesNewTokenWithSameFamily(): void { $token = $this->createToken(); $rotateAt = new DateTimeImmutable('2026-01-31 11:00:00'); [$newToken, $oldToken] = $token->rotate($rotateAt); // Nouveau token self::assertNotSame($token->id, $newToken->id); self::assertSame($token->familyId, $newToken->familyId); self::assertSame($token->userId, $newToken->userId); self::assertSame($token->id, $newToken->rotatedFrom); self::assertFalse($newToken->isRotated); // Ancien token marqué comme rotaté self::assertSame($token->id, $oldToken->id); self::assertTrue($oldToken->isRotated); } #[Test] public function isExpiredReturnsTrueWhenPastExpiration(): void { $issuedAt = new DateTimeImmutable('2026-01-31 10:00:00'); $token = RefreshToken::create( UserId::generate(), TenantId::fromString(self::TENANT_ID), DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'), $issuedAt, 3600, // 1 hour ); self::assertFalse($token->isExpired(new DateTimeImmutable('2026-01-31 10:30:00'))); self::assertTrue($token->isExpired(new DateTimeImmutable('2026-01-31 11:30:00'))); } #[Test] public function isInGracePeriodReturnsTrueWithin30SecondsOfRotation(): void { $token = $this->createToken(); $rotateAt = new DateTimeImmutable('2026-01-31 11:00:00'); [$_, $oldToken] = $token->rotate($rotateAt); // Dans la grace period (30s après rotation) self::assertTrue($oldToken->isInGracePeriod(new DateTimeImmutable('2026-01-31 11:00:15'))); self::assertTrue($oldToken->isInGracePeriod(new DateTimeImmutable('2026-01-31 11:00:30'))); // Après la grace period self::assertFalse($oldToken->isInGracePeriod(new DateTimeImmutable('2026-01-31 11:00:31'))); self::assertFalse($oldToken->isInGracePeriod(new DateTimeImmutable('2026-01-31 11:01:00'))); } #[Test] public function rotatePreservesOriginalTtl(): void { $issuedAt = new DateTimeImmutable('2026-01-31 10:00:00'); $originalTtl = 86400; // 1 day (web session) $token = RefreshToken::create( UserId::generate(), TenantId::fromString(self::TENANT_ID), DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'), $issuedAt, $originalTtl, ); $rotateAt = new DateTimeImmutable('2026-01-31 14:00:00'); [$newToken, $oldToken] = $token->rotate($rotateAt); // Le nouveau token doit avoir le même TTL que l'original $expectedExpiry = $rotateAt->modify("+{$originalTtl} seconds"); self::assertEquals($expectedExpiry, $newToken->expiresAt); // L'ancien token garde son expiration originale self::assertEquals($issuedAt->modify("+{$originalTtl} seconds"), $oldToken->expiresAt); // L'ancien token a rotatedAt défini self::assertEquals($rotateAt, $oldToken->rotatedAt); self::assertNull($newToken->rotatedAt); } #[Test] public function matchesDeviceReturnsTrueForSameFingerprint(): void { $fingerprint = DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'); $token = RefreshToken::create( UserId::generate(), TenantId::fromString(self::TENANT_ID), $fingerprint, new DateTimeImmutable(), ); $sameFingerprint = DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'); $differentFingerprint = DeviceFingerprint::fromRequest('Chrome/110', '10.0.0.1'); self::assertTrue($token->matchesDevice($sameFingerprint)); self::assertFalse($token->matchesDevice($differentFingerprint)); } #[Test] public function toTokenStringAndExtractIdRoundTrips(): void { $token = $this->createToken(); $tokenString = $token->toTokenString(); $extractedId = RefreshToken::extractIdFromTokenString($tokenString); self::assertEquals($token->id, $extractedId); } private function createToken(): RefreshToken { return RefreshToken::create( UserId::generate(), TenantId::fromString(self::TENANT_ID), DeviceFingerprint::fromRequest('Mozilla/5.0', '192.168.1.1'), new DateTimeImmutable('2026-01-31 10:00:00'), ); } }