diff --git a/.gitignore b/.gitignore index 990331b..ad588fe 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ composer.phar # PHPUnit cache .phpunit.result.cache +.phpunit.cache/ .vscode/launch.json .idea/* diff --git a/lib/Model/DevCycleUser.php b/lib/Model/DevCycleUser.php index d63c195..83cc789 100644 --- a/lib/Model/DevCycleUser.php +++ b/lib/Model/DevCycleUser.php @@ -713,18 +713,35 @@ public function toHeaderValue(): string public static function FromEvaluationContext(EvaluationContext $context): DevCycleUser { $user = new DevCycleUser(); - if ($context->getTargetingKey() === null && $context->getAttributes()->get("user_id") === null) { - throw new \InvalidArgumentException('targetingKey or user_id is missing from EvaluationContext'); - } - if ($context->getAttributes()->get("user_id") !== null) { - $userId = $context->getAttributes()->get("user_id"); - } else { + + // Priority order: targetingKey -> user_id -> userId + $userId = null; + $userIdSource = null; + + if ($context->getTargetingKey() !== null) { $userId = $context->getTargetingKey(); + $userIdSource = 'targetingKey'; + } elseif ($context->getAttributes()->get("user_id") !== null) { + $userId = $context->getAttributes()->get("user_id"); + $userIdSource = 'user_id'; + } elseif ($context->getAttributes()->get("userId") !== null) { + $userId = $context->getAttributes()->get("userId"); + $userIdSource = 'userId'; + } + + if ($userId === null) { + throw new \InvalidArgumentException('targetingKey, user_id, or userId is missing from EvaluationContext'); + } + + if (!ValueTypeValidator::isString($userId)) { + throw new \InvalidArgumentException("User ID field '{$userIdSource}' must be a string value, got " . gettype($userId)); } + $user->setUserId($userId); foreach ($context->getAttributes()->toArray() as $key => $value) { - if ($key === 'user_id' || $key === 'targetingKey') { + // Always skip all user ID field types to avoid duplication in custom data + if ($key === 'targetingKey' || $key === 'user_id' || $key === 'userId') { continue; } switch ($key) { diff --git a/test/Api/OpenFeatureTest.php b/test/Api/OpenFeatureTest.php index 006272b..205d793 100644 --- a/test/Api/OpenFeatureTest.php +++ b/test/Api/OpenFeatureTest.php @@ -27,22 +27,121 @@ final class OpenFeatureTest extends TestCase public function testEvaluationContextTargetingKey() { - + // Test targetingKey only $evalContextTK = new EvaluationContext(targetingKey: "test"); $user = DevCycleUser::FromEvaluationContext($evalContextTK); self::assertEquals("test", $user->getUserId(), 'User ID not properly passed through/set'); + // Test user_id only $attributes = new Attributes(array("user_id" => "test")); $evaluationContext = new EvaluationContext(attributes: $attributes); $user = DevCycleUser::FromEvaluationContext($evaluationContext); self::assertEquals("test", $user->getUserId(), 'User ID not properly passed through/set'); + // Test userId only + $attributes = new Attributes(array("userId" => "test")); + $evaluationContext = new EvaluationContext(attributes: $attributes); + $user = DevCycleUser::FromEvaluationContext($evaluationContext); + self::assertEquals("test", $user->getUserId(), 'User ID not properly passed through/set'); + + // Test priority: targetingKey should take precedence over user_id $attributes = new Attributes(array("user_id" => "test")); $evaluationContext = new EvaluationContext(targetingKey: "test2", attributes: $attributes); $user = DevCycleUser::FromEvaluationContext($evaluationContext); - self::assertEquals("test", $user->getUserId(), 'User ID not properly passed through/set'); + self::assertEquals("test2", $user->getUserId(), 'targetingKey should take precedence over user_id'); + + // Test priority: targetingKey should take precedence over userId + $attributes = new Attributes(array("userId" => "test")); + $evaluationContext = new EvaluationContext(targetingKey: "test2", attributes: $attributes); + $user = DevCycleUser::FromEvaluationContext($evaluationContext); + self::assertEquals("test2", $user->getUserId(), 'targetingKey should take precedence over userId'); + + // Test priority: user_id should take precedence over userId + $attributes = new Attributes(array("user_id" => "test", "userId" => "test2")); + $evaluationContext = new EvaluationContext(attributes: $attributes); + $user = DevCycleUser::FromEvaluationContext($evaluationContext); + self::assertEquals("test", $user->getUserId(), 'user_id should take precedence over userId'); + // Test all three present: targetingKey should win + $attributes = new Attributes(array("user_id" => "test", "userId" => "test2")); + $evaluationContext = new EvaluationContext(targetingKey: "test3", attributes: $attributes); + $user = DevCycleUser::FromEvaluationContext($evaluationContext); + self::assertEquals("test3", $user->getUserId(), 'targetingKey should take precedence over all other user ID fields'); } + + public function testUserIdFieldsNotInCustomData() + { + // Test that when user_id is used as main user ID, it doesn't appear in custom data + $attributes = new Attributes(array("user_id" => "test-user", "other_field" => "value")); + $evaluationContext = new EvaluationContext(attributes: $attributes); + $user = DevCycleUser::FromEvaluationContext($evaluationContext); + self::assertEquals("test-user", $user->getUserId(), 'user_id should be used as main user ID'); + self::assertArrayNotHasKey("user_id", $user->getCustomData(), 'user_id should not appear in custom data when used as main user ID'); + self::assertArrayHasKey("other_field", $user->getCustomData(), 'other fields should still appear in custom data'); + + // Test that when userId is used as main user ID, it doesn't appear in custom data + $attributes = new Attributes(array("userId" => "test-user", "other_field" => "value")); + $evaluationContext = new EvaluationContext(attributes: $attributes); + $user = DevCycleUser::FromEvaluationContext($evaluationContext); + self::assertEquals("test-user", $user->getUserId(), 'userId should be used as main user ID'); + self::assertArrayNotHasKey("userId", $user->getCustomData(), 'userId should not appear in custom data when used as main user ID'); + self::assertArrayHasKey("other_field", $user->getCustomData(), 'other fields should still appear in custom data'); + + // Test that all user ID fields are always excluded from custom data + $attributes = new Attributes(array("user_id" => "test-user-id", "userId" => "test-userId", "other_field" => "value")); + $evaluationContext = new EvaluationContext(targetingKey: "targeting-key", attributes: $attributes); + $user = DevCycleUser::FromEvaluationContext($evaluationContext); + self::assertEquals("targeting-key", $user->getUserId(), 'targetingKey should be used as main user ID'); + self::assertArrayNotHasKey("user_id", $user->getCustomData(), 'user_id should never appear in custom data'); + self::assertArrayNotHasKey("userId", $user->getCustomData(), 'userId should never appear in custom data'); + self::assertArrayHasKey("other_field", $user->getCustomData(), 'other fields should still appear in custom data'); + + // Test that all user ID fields are excluded even when not used as main user ID + $attributes = new Attributes(array("user_id" => "test-user-id", "userId" => "test-userId", "other_field" => "value")); + $evaluationContext = new EvaluationContext(attributes: $attributes); + $user = DevCycleUser::FromEvaluationContext($evaluationContext); + self::assertEquals("test-user-id", $user->getUserId(), 'user_id should be used as main user ID'); + self::assertArrayNotHasKey("user_id", $user->getCustomData(), 'user_id should never appear in custom data'); + self::assertArrayNotHasKey("userId", $user->getCustomData(), 'userId should never appear in custom data'); + self::assertArrayHasKey("other_field", $user->getCustomData(), 'other fields should still appear in custom data'); + } + + public function testMissingUserIdThrowsException() + { + // Test that missing all user ID fields throws exception + $attributes = new Attributes(array("other_field" => "value")); + $evaluationContext = new EvaluationContext(attributes: $attributes); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('targetingKey, user_id, or userId is missing from EvaluationContext'); + + DevCycleUser::FromEvaluationContext($evaluationContext); + } + + public function testInvalidUserIdTypeThrowsException() + { + // Test that non-string user_id throws string validation exception + $attributes = new Attributes(array("user_id" => array("invalid", "array"), "other_field" => "value")); + $evaluationContext = new EvaluationContext(attributes: $attributes); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("User ID field 'user_id' must be a string value, got array"); + + DevCycleUser::FromEvaluationContext($evaluationContext); + } + + public function testInvalidUserIdFieldTypeThrowsException() + { + // Test that non-string userId throws string validation exception with correct field name + $attributes = new Attributes(array("userId" => true, "other_field" => "value")); + $evaluationContext = new EvaluationContext(attributes: $attributes); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage("User ID field 'userId' must be a string value, got boolean"); + + DevCycleUser::FromEvaluationContext($evaluationContext); + } + public function testEvaluationContext() { $attributes = new Attributes(array(