id); self::assertSame($userId, $token->userId); self::assertSame($email, $token->email); self::assertTrue($tenantId->equals($token->tenantId)); self::assertSame($role, $token->role); self::assertSame($schoolName, $token->schoolName); self::assertEquals($now, $token->createdAt); self::assertFalse($token->isUsed()); } #[Test] public function generateRecordsActivationTokenGeneratedEvent(): void { $token = $this->createToken(); $events = $token->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(ActivationTokenGenerated::class, $events[0]); } #[Test] public function tokenValueIsUuidV7Format(): void { $token = $this->createToken(); self::assertMatchesRegularExpression( '/^[a-f0-9]{8}-[a-f0-9]{4}-7[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i', $token->tokenValue, ); } #[Test] public function expiresAtIs7DaysAfterCreation(): void { $createdAt = new DateTimeImmutable('2026-01-15 10:00:00'); $expectedExpiration = new DateTimeImmutable('2026-01-22 10:00:00'); $token = ActivationToken::generate( userId: self::USER_ID, email: self::EMAIL, tenantId: TenantId::fromString(self::TENANT_ID), role: self::ROLE, schoolName: self::SCHOOL_NAME, createdAt: $createdAt, ); self::assertEquals($expectedExpiration, $token->expiresAt); } #[Test] public function isExpiredReturnsFalseWhenNotExpired(): void { $createdAt = new DateTimeImmutable('2026-01-15 10:00:00'); $checkAt = new DateTimeImmutable('2026-01-20 10:00:00'); $token = ActivationToken::generate( userId: self::USER_ID, email: self::EMAIL, tenantId: TenantId::fromString(self::TENANT_ID), role: self::ROLE, schoolName: self::SCHOOL_NAME, createdAt: $createdAt, ); self::assertFalse($token->isExpired($checkAt)); } #[Test] public function isExpiredReturnsTrueWhenExpired(): void { $createdAt = new DateTimeImmutable('2026-01-15 10:00:00'); $checkAt = new DateTimeImmutable('2026-01-25 10:00:00'); $token = ActivationToken::generate( userId: self::USER_ID, email: self::EMAIL, tenantId: TenantId::fromString(self::TENANT_ID), role: self::ROLE, schoolName: self::SCHOOL_NAME, createdAt: $createdAt, ); self::assertTrue($token->isExpired($checkAt)); } #[Test] public function isExpiredReturnsTrueAtExactExpirationMoment(): void { $createdAt = new DateTimeImmutable('2026-01-15 10:00:00'); $checkAt = new DateTimeImmutable('2026-01-22 10:00:00'); $token = ActivationToken::generate( userId: self::USER_ID, email: self::EMAIL, tenantId: TenantId::fromString(self::TENANT_ID), role: self::ROLE, schoolName: self::SCHOOL_NAME, createdAt: $createdAt, ); self::assertTrue($token->isExpired($checkAt)); } #[Test] public function useMarksTokenAsUsed(): void { $token = $this->createToken(); $usedAt = new DateTimeImmutable('2026-01-16 10:00:00'); $token->use($usedAt); self::assertTrue($token->isUsed()); self::assertEquals($usedAt, $token->usedAt); } #[Test] public function useRecordsActivationTokenUsedEvent(): void { $token = $this->createToken(); $token->pullDomainEvents(); $usedAt = new DateTimeImmutable('2026-01-16 10:00:00'); $token->use($usedAt); $events = $token->pullDomainEvents(); self::assertCount(1, $events); self::assertInstanceOf(ActivationTokenUsed::class, $events[0]); } #[Test] public function useThrowsExceptionWhenTokenAlreadyUsed(): void { $token = $this->createToken(); $firstUse = new DateTimeImmutable('2026-01-16 10:00:00'); $token->use($firstUse); $this->expectException(ActivationTokenAlreadyUsedException::class); $secondUse = new DateTimeImmutable('2026-01-17 10:00:00'); $token->use($secondUse); } #[Test] public function useThrowsExceptionWhenTokenExpired(): void { $createdAt = new DateTimeImmutable('2026-01-15 10:00:00'); $usedAt = new DateTimeImmutable('2026-01-25 10:00:00'); $token = ActivationToken::generate( userId: self::USER_ID, email: self::EMAIL, tenantId: TenantId::fromString(self::TENANT_ID), role: self::ROLE, schoolName: self::SCHOOL_NAME, createdAt: $createdAt, ); $this->expectException(ActivationTokenExpiredException::class); $token->use($usedAt); } #[Test] public function generateStoresStudentIdWhenProvided(): void { $studentId = '550e8400-e29b-41d4-a716-446655440099'; $token = ActivationToken::generate( userId: self::USER_ID, email: self::EMAIL, tenantId: TenantId::fromString(self::TENANT_ID), role: self::ROLE, schoolName: self::SCHOOL_NAME, createdAt: new DateTimeImmutable('2026-01-15 10:00:00'), studentId: $studentId, ); self::assertSame($studentId, $token->studentId); } #[Test] public function generateHasNullStudentIdByDefault(): void { $token = $this->createToken(); self::assertNull($token->studentId); } #[Test] public function reconstitutePreservesStudentId(): void { $studentId = '550e8400-e29b-41d4-a716-446655440099'; $token = ActivationToken::reconstitute( id: ActivationTokenId::fromString('550e8400-e29b-41d4-a716-446655440010'), tokenValue: 'some-token-value', userId: self::USER_ID, email: self::EMAIL, tenantId: TenantId::fromString(self::TENANT_ID), role: self::ROLE, schoolName: self::SCHOOL_NAME, createdAt: new DateTimeImmutable('2026-01-15 10:00:00'), expiresAt: new DateTimeImmutable('2026-01-22 10:00:00'), usedAt: null, studentId: $studentId, ); self::assertSame($studentId, $token->studentId); } private function createToken(): ActivationToken { return ActivationToken::generate( userId: self::USER_ID, email: self::EMAIL, tenantId: TenantId::fromString(self::TENANT_ID), role: self::ROLE, schoolName: self::SCHOOL_NAME, createdAt: new DateTimeImmutable('2026-01-15 10:00:00'), ); } }