repository = new class implements FeatureFlagRepository { private array $flags = []; public function find(string $name): ?FeatureFlag { return $this->flags[$name] ?? null; } public function findAll(): array { return array_values($this->flags); } public function save(FeatureFlag $flag): void { $this->flags[$flag->name] = $flag; } public function delete(string $name): void { unset($this->flags[$name]); } public function exists(string $name): bool { return isset($this->flags[$name]); } }; $this->manager = new FeatureFlagManager($this->repository); }); it('enables a feature flag', function () { $this->manager->enable('test-feature', 'Test description'); expect($this->manager->isEnabled('test-feature'))->toBeTrue(); expect($this->manager->exists('test-feature'))->toBeTrue(); }); it('disables a feature flag', function () { $this->manager->enable('test-feature'); $this->manager->disable('test-feature'); expect($this->manager->isDisabled('test-feature'))->toBeTrue(); }); it('returns false for non-existent flag', function () { expect($this->manager->isEnabled('non-existent'))->toBeFalse(); }); it('sets conditional feature flag', function () { $this->manager->setConditional('user-feature', ['user_id' => [1, 2, 3]]); $matchingContext = (new FeatureFlagContext())->withUserId('2'); $nonMatchingContext = (new FeatureFlagContext())->withUserId('99'); expect($this->manager->isEnabled('user-feature', $matchingContext))->toBeTrue(); expect($this->manager->isEnabled('user-feature', $nonMatchingContext))->toBeFalse(); }); it('sets percentage rollout', function () { $this->manager->setPercentageRollout('rollout-feature', 50); $flag = $this->manager->getFlag('rollout-feature'); expect($flag)->not->toBeNull(); expect($flag->conditions)->toHaveKey('percentage'); expect($flag->conditions['percentage'])->toBe(50); }); it('validates percentage range', function () { expect(fn () => $this->manager->setPercentageRollout('invalid', 150)) ->toThrow(\InvalidArgumentException::class); expect(fn () => $this->manager->setPercentageRollout('invalid', -10)) ->toThrow(\InvalidArgumentException::class); }); it('sets user-specific flag', function () { $this->manager->setForUsers('user-only', [1, 2, 3]); $flag = $this->manager->getFlag('user-only'); expect($flag->conditions)->toHaveKey('user_id'); expect($flag->conditions['user_id'])->toBe([1, 2, 3]); }); it('sets environment-specific flag', function () { $this->manager->setForEnvironment('env-feature', 'production'); $prodContext = (new FeatureFlagContext())->withEnvironment('production'); $devContext = (new FeatureFlagContext())->withEnvironment('development'); expect($this->manager->isEnabled('env-feature', $prodContext))->toBeTrue(); expect($this->manager->isEnabled('env-feature', $devContext))->toBeFalse(); }); it('sets expiration', function () { $this->manager->enable('temp-feature'); $expiry = Timestamp::fromTimestamp(time() + 3600); $this->manager->setExpiration('temp-feature', $expiry); $flag = $this->manager->getFlag('temp-feature'); expect($flag->expiresAt)->toBe($expiry); }); it('throws exception when setting expiration on non-existent flag', function () { $expiry = Timestamp::fromTimestamp(time() + 3600); expect(fn () => $this->manager->setExpiration('non-existent', $expiry)) ->toThrow(\InvalidArgumentException::class); }); it('deletes a feature flag', function () { $this->manager->enable('delete-me'); expect($this->manager->exists('delete-me'))->toBeTrue(); $this->manager->deleteFlag('delete-me'); expect($this->manager->exists('delete-me'))->toBeFalse(); }); it('gets all feature flags', function () { $this->manager->enable('flag1'); $this->manager->enable('flag2'); $this->manager->disable('flag3'); $allFlags = $this->manager->getAllFlags(); expect($allFlags)->toHaveCount(3); }); it('generates status summary', function () { $this->manager->enable('enabled1'); $this->manager->enable('enabled2'); $this->manager->disable('disabled1'); $this->manager->setConditional('conditional1', ['user_id' => [1]]); $summary = $this->manager->getStatusSummary(); expect($summary['total'])->toBe(4); expect($summary['enabled'])->toBe(2); expect($summary['disabled'])->toBe(1); expect($summary['conditional'])->toBe(1); }); it('uses default context when none provided', function () { $defaultContext = (new FeatureFlagContext())->withUserId('5'); $manager = new FeatureFlagManager($this->repository, $defaultContext); $manager->setConditional('user-feature', ['user_id' => [5, 6, 7]]); // Should use default context expect($manager->isEnabled('user-feature'))->toBeTrue(); }); it('counts expired flags in summary', function () { $this->manager->enable('expired-feature'); $pastExpiry = Timestamp::fromTimestamp(time() - 3600); $this->manager->setExpiration('expired-feature', $pastExpiry); $summary = $this->manager->getStatusSummary(); expect($summary['expired'])->toBe(1); }); });