isEnabled())->toBeTrue(); expect($flag->name)->toBe('test-feature'); expect($flag->description)->toBe('Test feature'); }); it('creates disabled feature flag', function () { $flag = new FeatureFlag( name: 'test-feature', status: FeatureFlagStatus::DISABLED ); expect($flag->isEnabled())->toBeFalse(); }); it('checks expiration', function () { $pastTime = Timestamp::fromTimestamp(time() - 3600); $flag = new FeatureFlag( name: 'expired-feature', status: FeatureFlagStatus::ENABLED, expiresAt: $pastTime ); expect($flag->isEnabled())->toBeFalse(); }); it('enables flag for matching context', function () { $flag = new FeatureFlag( name: 'user-feature', status: FeatureFlagStatus::CONDITIONAL, conditions: ['user_id' => [1, 2, 3]] ); $context = (new FeatureFlagContext())->withUserId('2'); expect($flag->isEnabledForContext($context))->toBeTrue(); }); it('disables flag for non-matching context', function () { $flag = new FeatureFlag( name: 'user-feature', status: FeatureFlagStatus::CONDITIONAL, conditions: ['user_id' => [1, 2, 3]] ); $context = (new FeatureFlagContext())->withUserId('99'); expect($flag->isEnabledForContext($context))->toBeFalse(); }); it('handles percentage rollout', function () { $flag = new FeatureFlag( name: 'rollout-feature', status: FeatureFlagStatus::CONDITIONAL, conditions: ['percentage' => 50] ); // Test with multiple user IDs to verify distribution $enabledCount = 0; for ($userId = 1; $userId <= 100; $userId++) { $context = (new FeatureFlagContext())->withUserId((string) $userId); if ($flag->isEnabledForContext($context)) { $enabledCount++; } } // Should be approximately 50% (allow some variance) expect($enabledCount)->toBeGreaterThan(30); expect($enabledCount)->toBeLessThan(70); }); it('creates immutable copy with new status', function () { $original = new FeatureFlag( name: 'test-feature', status: FeatureFlagStatus::DISABLED ); $updated = $original->withStatus(FeatureFlagStatus::ENABLED); expect($original->status)->toBe(FeatureFlagStatus::DISABLED); expect($updated->status)->toBe(FeatureFlagStatus::ENABLED); expect($original)->not->toBe($updated); }); it('creates immutable copy with new conditions', function () { $original = new FeatureFlag( name: 'test-feature', status: FeatureFlagStatus::CONDITIONAL ); $updated = $original->withConditions(['user_id' => [1, 2]]); expect($original->conditions)->toBeEmpty(); expect($updated->conditions)->toBe(['user_id' => [1, 2]]); }); it('creates immutable copy with expiration', function () { $original = new FeatureFlag( name: 'test-feature', status: FeatureFlagStatus::ENABLED ); $expiry = Timestamp::fromTimestamp(time() + 3600); $updated = $original->withExpiration($expiry); expect($original->expiresAt)->toBeNull(); expect($updated->expiresAt)->toBe($expiry); }); it('handles multiple conditions', function () { $flag = new FeatureFlag( name: 'multi-condition', status: FeatureFlagStatus::CONDITIONAL, conditions: [ 'environment' => 'production', 'user_id' => [1, 2, 3] ] ); $matchingContext = (new FeatureFlagContext()) ->withEnvironment('production') ->withUserId('2'); $nonMatchingContext = (new FeatureFlagContext()) ->withEnvironment('development') ->withUserId('2'); expect($flag->isEnabledForContext($matchingContext))->toBeTrue(); expect($flag->isEnabledForContext($nonMatchingContext))->toBeFalse(); }); });