expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('rolloutPercentage must be between 0 and 100'); new Flag('invalid', rolloutPercentage: 101); } public function testFlagValidationNegative(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('rolloutPercentage must be between 0 and 100'); new Flag('invalid', rolloutPercentage: -1); } public function testHydratorMissingFields(): void { $hydrator = new FlagHydrator(); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid rule definition for flag "test". Missing attribute, operator, or value.'); $hydrator->hydrate('test', [ 'rules' => [ ['attribute' => 'plan'] // missing operator and value ] ]); } public function testHydratorInvalidOperator(): void { $hydrator = new FlagHydrator(); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid operator "invalid_op" for flag "test".'); $hydrator->hydrate('test', [ 'rules' => [ ['attribute' => 'plan', 'operator' => 'invalid_op', 'value' => 'pro'] ] ]); } /** * @dataProvider operatorProvider */ public function testAllOperators(string $operator, mixed $ruleValue, mixed $contextValue, bool $expected): void { $evaluator = new Evaluator(); $flag = new Flag('test', rules: [ new Rule('attr', $operator, $ruleValue) ]); $context = Context::fromArray(['attr' => $contextValue]); $this->assertSame($expected, $evaluator->evaluate($flag, $context)); } public static function operatorProvider(): array { return [ 'eq match' => ['eq', 'val', 'val', true], 'eq mismatch' => ['eq', 'val', 'other', false], 'neq match' => ['neq', 'val', 'other', true], 'neq mismatch' => ['neq', 'val', 'val', false], 'gt match' => ['gt', 10, 11, true], 'gt mismatch' => ['gt', 10, 10, false], 'gte match eq' => ['gte', 10, 10, true], 'gte match gt' => ['gte', 10, 11, true], 'gte mismatch' => ['gte', 10, 9, false], 'lt match' => ['lt', 10, 9, true], 'lt mismatch' => ['lt', 10, 10, false], 'lte match eq' => ['lte', 10, 10, true], 'lte match lt' => ['lte', 10, 9, true], 'lte mismatch' => ['lte', 10, 11, false], 'in match' => ['in', ['a', 'b'], 'a', true], 'in mismatch' => ['in', ['a', 'b'], 'c', false], 'nin match' => ['nin', ['a', 'b'], 'c', true], 'nin mismatch' => ['nin', ['a', 'b'], 'a', false], 'contains match' => ['contains', 'foo', 'foobar', true], 'contains mismatch' => ['contains', 'baz', 'foobar', false], ]; } public function testRuntimeUnsupportedOperator(): void { $evaluator = new Evaluator(); // We bypass the hydrator which has validation to test the evaluator safety $flag = new Flag('test', rules: [ new Rule('attr', 'unsupported', 'val') ]); $context = Context::fromArray(['attr' => 'val']); $this->expectException(\LogicException::class); $this->expectExceptionMessage('Unsupported operator "unsupported" encountered during evaluation.'); $evaluator->evaluate($flag, $context); } }