diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..f8a529b --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,10 @@ +--- +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + reviewers: + - "splitio/sdk" diff --git a/.gitignore b/.gitignore index 7e16f04..2a46124 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor .phpunit.cache coverage.xml coverage.html/ +*.local.php diff --git a/CHANGES b/CHANGES index 50fff84..3eae72b 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,11 @@ +1.5.0 (Jan 25, 2024) +- Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): + - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. + - getTreatmentsByFlagSet and getTreatmentsByFlagSets + - getTreatmentWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets +- Updated the following SDK manager method to expose flag sets on flag views: + - Added `defaultTreatment` property to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager. + 1.4.0 (Dec 14, 2023): - Add support for a custom tracer for client methods. - Support finer granularity on timeouts. diff --git a/composer.lock b/composer.lock index fb0bef4..a5acc67 100644 --- a/composer.lock +++ b/composer.lock @@ -1923,4 +1923,4 @@ "platform": [], "platform-dev": [], "plugin-api-version": "2.3.0" -} +} \ No newline at end of file diff --git a/examples/treatmentsByFlagSet.php b/examples/treatmentsByFlagSet.php new file mode 100644 index 0000000..8dd6758 --- /dev/null +++ b/examples/treatmentsByFlagSet.php @@ -0,0 +1,37 @@ +getKey() + ." feat=".$i->getFeature() + ." treatment=".$i->getTreatment() + ." label=".$i->getLabel() + ." cn=".$i->getChangeNumber() + ." #attrs=".(($a == null) ? 0 : count($a))."\n"; + } +} + +$factory = Factory::withConfig([ + 'transfer' => [ + 'address' => '../../splitd/splitd.sock', + 'type' => 'unix-stream', + ], + 'logging' => [ + 'level' => \Psr\Log\LogLevel::INFO, + ], + 'utils' => [ + 'impressionListener' => new CustomListener(), + ], +]); + +$client = $factory->client(); +print_r($client->getTreatmentsByFlagSet("key", null, "server_side", ['age' => 22])); +print_r($client->getTreatmentsWithConfigByFlagSet("key", null, "server_side", ['age' => 22])); diff --git a/examples/treatmentsByFlagSets.php b/examples/treatmentsByFlagSets.php new file mode 100644 index 0000000..bcfb39c --- /dev/null +++ b/examples/treatmentsByFlagSets.php @@ -0,0 +1,37 @@ +getKey() + ." feat=".$i->getFeature() + ." treatment=".$i->getTreatment() + ." label=".$i->getLabel() + ." cn=".$i->getChangeNumber() + ." #attrs=".(($a == null) ? 0 : count($a))."\n"; + } +} + +$factory = Factory::withConfig([ + 'transfer' => [ + 'address' => '../../splitd/splitd.sock', + 'type' => 'unix-stream', + ], + 'logging' => [ + 'level' => \Psr\Log\LogLevel::INFO, + ], + 'utils' => [ + 'impressionListener' => new CustomListener(), + ], +]); + +$client = $factory->client(); +print_r($client->getTreatmentsByFlagSets("key", null, ["server_side", "backend"], ['age' => 22])); +print_r($client->getTreatmentsWithConfigByFlagSets("key", null, ["server_side", "backend"], ['age' => 22])); diff --git a/src/Client.php b/src/Client.php index d1a5d3c..a12b41a 100644 --- a/src/Client.php +++ b/src/Client.php @@ -129,7 +129,6 @@ public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, arra $this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : [])); $toReturn = $this->cache->getManyWithConfig($key, $features, $attributes); $features = self::getMissing($toReturn); - if (count($features) == 0) { return $toReturn; } @@ -156,6 +155,182 @@ public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, arra } } + public function getTreatmentsByFlagSet( + string $key, + ?string $bucketingKey, + string $flagSet, + ?array $attributes + ): array { + try { + $id = $this->tracer->makeId(); + $method = Tracer::METHOD_GET_TREATMENTS_BY_FLAG_SET; + $this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : [])); + $flagSet = $this->inputValidator->sanitize($flagSet, 'getTreatmentsByFlagSet'); + if (is_null($flagSet)) { + return array(); + } + $featuresFromSet = $this->cache->getFeaturesByFlagSets([$flagSet]); + if (!is_null($featuresFromSet)) { + $toReturn = $this->cache->getMany($key, $featuresFromSet, $attributes); + $features = self::getMissing($toReturn); + if (count($features) == 0) { + return $toReturn; + } + } + + $this->tracer->trace(TEF::forRPCStart($method, $id)); + $results = $this->lm->getTreatmentsByFlagSet($key, $bucketingKey, $flagSet, $attributes); + $this->tracer->trace(TEF::forRPCEnd($method, $id)); + $toReturn = array(); + foreach ($results as $feature => $result) { + list($treatment, $ilData) = $result; + $toReturn[$feature] = $treatment; + $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); + } + $this->cache->setFeaturesForFlagSets([$flagSet], array_keys($results)); + $this->cache->setMany($key, $attributes, $toReturn); + return $toReturn; + } catch (\Exception $exc) { + $this->tracer->trace(TEF::forException($method, $id, $exc)); + $this->logger->error($exc); + return array(); + } finally { + $this->tracer->trace(TEF::forEnd($method, $id)); + } + } + + public function getTreatmentsWithConfigByFlagSet( + string $key, + ?string $bucketingKey, + string $flagSet, + ?array $attributes = null + ): array { + try { + $id = $this->tracer->makeId(); + $method = Tracer::METHOD_GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET; + $this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : [])); + $flagSet = $this->inputValidator->sanitize($flagSet, 'getTreatmentsWithConfigByFlagSet'); + if (is_null($flagSet)) { + return array(); + } + $featuresFromSet = $this->cache->getFeaturesByFlagSets([$flagSet]); + if (!is_null($featuresFromSet)) { + $toReturn = $this->cache->getManyWithConfig($key, $featuresFromSet, $attributes); + $features = self::getMissing($toReturn); + if (count($features) == 0) { + return $toReturn; + } + } + + $this->tracer->trace(TEF::forRPCStart($method, $id)); + $results = $this->lm->getTreatmentsWithConfigByFlagSet($key, $bucketingKey, $flagSet, $attributes); + $this->tracer->trace(TEF::forRPCEnd($method, $id)); + $toReturn = array(); + foreach ($results as $feature => $result) { + list($treatment, $ilData, $config) = $result; + $toReturn[$feature] = ['treatment' => $treatment, 'config' => $config]; + $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); + } + $this->cache->setFeaturesForFlagSets([$flagSet], array_keys($results)); + $this->cache->setManyWithConfig($key, $attributes, $toReturn); + return $toReturn; + } catch (\Exception $exc) { + $this->tracer->trace(TEF::forException($method, $id, $exc)); + $this->logger->error($exc); + return array(); + } finally { + $this->tracer->trace(TEF::forEnd($method, $id)); + } + } + + public function getTreatmentsByFlagSets( + string $key, + ?string $bucketingKey, + array $flagSets, + ?array $attributes + ): array { + try { + $id = $this->tracer->makeId(); + $method = Tracer::METHOD_GET_TREATMENTS_BY_FLAG_SETS; + $this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : [])); + $flagSets = $this->inputValidator->sanitizeMany($flagSets, 'getTreatmentsByFlagSets'); + if (is_null($flagSets)) { + return array(); + } + $featuresFromSets = $this->cache->getFeaturesByFlagSets($flagSets); + if (!is_null($featuresFromSets)) { + $toReturn = $this->cache->getMany($key, $featuresFromSets, $attributes); + $features = self::getMissing($toReturn); + if (count($features) == 0) { + return $toReturn; + } + } + + $this->tracer->trace(TEF::forRPCStart($method, $id)); + $results = $this->lm->getTreatmentsByFlagSets($key, $bucketingKey, $flagSets, $attributes); + $this->tracer->trace(TEF::forRPCEnd($method, $id)); + $toReturn = array(); + foreach ($results as $feature => $result) { + list($treatment, $ilData) = $result; + $toReturn[$feature] = $treatment; + $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); + } + $this->cache->setFeaturesForFlagSets($flagSets, array_keys($results)); + $this->cache->setMany($key, $attributes, $toReturn); + return $toReturn; + } catch (\Exception $exc) { + $this->tracer->trace(TEF::forException($method, $id, $exc)); + $this->logger->error($exc); + return array(); + } finally { + $this->tracer->trace(TEF::forEnd($method, $id)); + } + } + + public function getTreatmentsWithConfigByFlagSets( + string $key, + ?string $bucketingKey, + array $flagSets, + ?array $attributes = null + ): array { + try { + $id = $this->tracer->makeId(); + $method = Tracer::METHOD_GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS; + $this->tracer->trace(TEF::forStart($method, $id, $this->tracer->includeArgs() ? func_get_args() : [])); + $flagSets = $this->inputValidator->sanitizeMany($flagSets, 'getTreatmentsWithConfigByFlagSets'); + if (is_null($flagSets)) { + return array(); + } + $featuresFromSet = $this->cache->getFeaturesByFlagSets($flagSets); + if (!is_null($featuresFromSet)) { + $toReturn = $this->cache->getManyWithConfig($key, $featuresFromSet, $attributes); + $features = self::getMissing($toReturn); + if (count($features) == 0) { + return $toReturn; + } + } + + $this->tracer->trace(TEF::forRPCStart($method, $id)); + $results = $this->lm->getTreatmentsWithConfigByFlagSets($key, $bucketingKey, $flagSets, $attributes); + $this->tracer->trace(TEF::forRPCEnd($method, $id)); + $toReturn = array(); + foreach ($results as $feature => $result) { + list($treatment, $ilData, $config) = $result; + $toReturn[$feature] = ['treatment' => $treatment, 'config' => $config]; + $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); + } + $this->cache->setFeaturesForFlagSets($flagSets, array_keys($results)); + $this->cache->setManyWithConfig($key, $attributes, $toReturn); + return $toReturn; + } catch (\Exception $exc) { + $this->tracer->trace(TEF::forException($method, $id, $exc)); + $this->logger->error($exc); + return array(); + } finally { + $this->tracer->trace(TEF::forEnd($method, $id)); + } + } + public function track(string $key, string $trafficType, string $eventType, ?float $value = null, ?array $properties = null): bool { try { diff --git a/src/ClientInterface.php b/src/ClientInterface.php index e62f045..4627be2 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -8,5 +8,19 @@ function getTreatment(string $key, ?string $bucketingKey, string $feature, ?arra function getTreatmentWithConfig(string $key, ?string $bucketingKey, string $feature, ?array $attributes): array; function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes): array; function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes): array; + function getTreatmentsByFlagSet(string $key, ?string $bucketingKey, string $flagSet, ?array $attributes): array; + function getTreatmentsWithConfigByFlagSet( + string $key, + ?string $bucketingKey, + string $flagSet, + ?array $attributes + ): array; + function getTreatmentsByFlagSets(string $key, ?string $bucketingKey, array $flagSets, ?array $attributes): array; + function getTreatmentsWithConfigByFlagSets( + string $key, + ?string $bucketingKey, + array $flagSets, + ?array $attributes + ): array; function track(string $key, string $trafficType, string $eventType, ?float $value = null, ?array $properties = null): bool; } diff --git a/src/Fallback/AlwaysControlClient.php b/src/Fallback/AlwaysControlClient.php index 2020e3e..97ed6f0 100644 --- a/src/Fallback/AlwaysControlClient.php +++ b/src/Fallback/AlwaysControlClient.php @@ -16,7 +16,7 @@ public function getTreatmentWithConfig(string $key, ?string $bucketingKey, strin return ['treatment' => 'control', 'config' => null]; } - public function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes): array + public function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes): array { return array_reduce($features, function ($carry, $item) { $carry[$item] = "control"; @@ -24,7 +24,7 @@ public function getTreatments(string $key, ?string $bucketingKey, array $featur }, []); } - public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes): array + public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes): array { return array_reduce($features, function ($carry, $item) { $carry[$item] = ['treatment' => 'control', 'config' => null]; @@ -32,6 +32,26 @@ public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, arr }, []); } + public function getTreatmentsByFlagSet(string $key, $bucketingKey, string $flagSet, $attributes): array + { + return array(); + } + + public function getTreatmentsWithConfigByFlagSet(string $key, $bucketingKey, string $flagSet, $attributes): array + { + return array(); + } + + public function getTreatmentsByFlagSets(string $key, $bucketingKey, array $flagSets, $attributes): array + { + return array(); + } + + public function getTreatmentsWithConfigByFlagSets(string $key, $bucketingKey, array $flagSets, $attributes): array + { + return array(); + } + public function track(string $key, string $trafficType, string $eventType, ?float $value = null, ?array $properties = null): bool { return false; diff --git a/src/Link/Consumer/Manager.php b/src/Link/Consumer/Manager.php index cdba27e..a3341b8 100644 --- a/src/Link/Consumer/Manager.php +++ b/src/Link/Consumer/Manager.php @@ -10,6 +10,20 @@ function getTreatment(string $key, ?string $bucketingKey, string $feature, ?arra function getTreatmentWithConfig(string $key, ?string $bucketingKey, string $feature, ?array $attributes): array; function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes): array; function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes): array; + function getTreatmentsByFlagSet(string $key, ?string $bucketingKey, string $flagSet, ?array $attributes): array; + function getTreatmentsWithConfigByFlagSet( + string $key, + ?string $bucketingKey, + string $flagSet, + ?array $attributes + ): array; + function getTreatmentsByFlagSets(string $key, ?string $bucketingKey, array $flagSets, ?array $attributes): array; + function getTreatmentsWithConfigByFlagSets( + string $key, + ?string $bucketingKey, + array $flagSet, + ?array $attributes + ): array; function track(string $key, string $trafficType, string $eventType, ?float $value, ?array $properties): bool; function splitNames(): array; function split(string $splitName): ?SplitView; diff --git a/src/Link/Consumer/V1Manager.php b/src/Link/Consumer/V1Manager.php index f9abf16..874558b 100644 --- a/src/Link/Consumer/V1Manager.php +++ b/src/Link/Consumer/V1Manager.php @@ -95,6 +95,94 @@ public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, arra return $results; } + public function getTreatmentsByFlagSet( + string $key, + ?string $bucketingKey, + string $flagSet, + ?array $attributes): array + { + $response = Protocol\V1\TreatmentsByFlagSetResponse::fromRaw( + $this->rpcWithReconnect(RPC::forTreatmentsByFlagSet($key, $bucketingKey, $flagSet, $attributes)) + ); + $response->ensureSuccess(); + + $results = []; + + foreach ($response->getEvaluationResults() as $feature => $evalResult) { + $results[$feature] = $evalResult == null + ? ["control", null] + : [$evalResult->getTreatment(), $evalResult->getImpressionListenerdata()]; + } + + return $results; + } + + public function getTreatmentsWithConfigByFlagSet( + string $key, + ?string $bucketingKey, + string $flagSet, + ?array $attributes + ): array { + $response = Protocol\V1\TreatmentsByFlagSetResponse::fromRaw( + $this->rpcWithReconnect(RPC::forTreatmentsWithConfigByFlagSet($key, $bucketingKey, $flagSet, $attributes)) + ); + $response->ensureSuccess(); + + $results = []; + + foreach ($response->getEvaluationResults() as $feature => $evalResult) { + $results[$feature] = $evalResult == null + ? ["control", null, null] + : [$evalResult->getTreatment(), $evalResult->getImpressionListenerdata(), $evalResult->getConfig()]; + } + + return $results; + } + + public function getTreatmentsByFlagSets( + string $key, + ?string $bucketingKey, + array $flagSets, + ?array $attributes): array + { + $response = Protocol\V1\TreatmentsByFlagSetResponse::fromRaw( + $this->rpcWithReconnect(RPC::forTreatmentsByFlagSets($key, $bucketingKey, $flagSets, $attributes)) + ); + $response->ensureSuccess(); + + $results = []; + + foreach ($response->getEvaluationResults() as $feature => $evalResult) { + $results[$feature] = $evalResult == null + ? ["control", null] + : [$evalResult->getTreatment(), $evalResult->getImpressionListenerdata()]; + } + + return $results; + } + + public function getTreatmentsWithConfigByFlagSets( + string $key, + ?string $bucketingKey, + array $flagSets, + ?array $attributes + ): array { + $response = Protocol\V1\TreatmentsByFlagSetResponse::fromRaw( + $this->rpcWithReconnect(RPC::forTreatmentsWithConfigByFlagSets($key, $bucketingKey, $flagSets, $attributes)) + ); + $response->ensureSuccess(); + + $results = []; + + foreach ($response->getEvaluationResults() as $feature => $evalResult) { + $results[$feature] = $evalResult == null + ? ["control", null, null] + : [$evalResult->getTreatment(), $evalResult->getImpressionListenerdata(), $evalResult->getConfig()]; + } + + return $results; + } + public function track(string $key, string $trafficType, string $eventType, ?float $value, ?array $properties): bool { $response = Protocol\V1\TrackResponse::fromRaw( @@ -163,6 +251,8 @@ private static function splitResultToView(SplitViewResult $res): SplitView $res->getKilled(), $res->getTreatments(), $res->getChangeNumber(), + $res->getDefaultTreatment(), + $res->getSets(), $res->getConfigs() ); } diff --git a/src/Link/Protocol/V1/OpCode.php b/src/Link/Protocol/V1/OpCode.php index e8c105a..404946e 100644 --- a/src/Link/Protocol/V1/OpCode.php +++ b/src/Link/Protocol/V1/OpCode.php @@ -26,6 +26,10 @@ class OpCode extends Enum private const Treatments = 0x12; private const TreatmentWithConfig = 0x13; private const TreatmentsWithConfig = 0x14; + private const TreatmentsByFlagSet = 0x15; + private const TreatmentsWithConfigByFlagSet = 0x16; + private const TreatmentsByFlagSets = 0x17; + private const TreatmentsWithConfigByFlagSets = 0x18; private const Track = 0x80; diff --git a/src/Link/Protocol/V1/RPC.php b/src/Link/Protocol/V1/RPC.php index 3b63438..ad57339 100644 --- a/src/Link/Protocol/V1/RPC.php +++ b/src/Link/Protocol/V1/RPC.php @@ -67,6 +67,42 @@ public static function forTreatmentsWithConfig(string $key, ?string $bucketingKe return self::_forTreatments($key, $bucketingKey, $features, $attributes, true); } + public static function forTreatmentsByFlagSet( + string $key, + ?string $bucketingKey, + string $flagSet, + ?array $attributes): RPC + { + return self::_forTreatmentsByFlagSet($key, $bucketingKey, $flagSet, $attributes, false); + } + + public static function forTreatmentsWithConfigByFlagSet( + string $key, + ?string $bucketingKey, + string $flagSet, + ?array $attributes): RPC + { + return self::_forTreatmentsByFlagSet($key, $bucketingKey, $flagSet, $attributes, true); + } + + public static function forTreatmentsByFlagSets( + string $key, + ?string $bucketingKey, + array $flagSets, + ?array $attributes): RPC + { + return self::_forTreatmentsByFlagSets($key, $bucketingKey, $flagSets, $attributes, false); + } + + public static function forTreatmentsWithConfigByFlagSets( + string $key, + ?string $bucketingKey, + array $flagSets, + ?array $attributes): RPC + { + return self::_forTreatmentsByFlagSets($key, $bucketingKey, $flagSets, $attributes, true); + } + public static function forTrack( string $key, string $trafficType, @@ -138,4 +174,42 @@ public static function _forTreatments(string $k, ?string $bk, array $f, ?array $ ] ); } + + public static function _forTreatmentsByFlagSet( + string $k, + ?string $bk, + string $f, + ?array $a, + bool $includeConfig): RPC + { + return new RPC( + Version::V1(), + $includeConfig ? OpCode::TreatmentsWithConfigByFlagSet() : OpCode::TreatmentsByFlagSet(), + [ + TreatmentsByFlagSetArgs::KEY()->getValue() => $k, + TreatmentsByFlagSetArgs::BUCKETING_KEY()->getValue() => $bk, + TreatmentsByFlagSetArgs::FLAG_SET()->getValue() => $f, + TreatmentsByFlagSetArgs::ATTRIBUTES()->getValue() => ($a != null && count($a) > 0) ? $a : null, + ] + ); + } + + public static function _forTreatmentsByFlagSets( + string $k, + ?string $bk, + array $f, + ?array $a, + bool $includeConfig): RPC + { + return new RPC( + Version::V1(), + $includeConfig ? OpCode::TreatmentsWithConfigByFlagSets() : OpCode::TreatmentsByFlagSets(), + [ + TreatmentsByFlagSetsArgs::KEY()->getValue() => $k, + TreatmentsByFlagSetsArgs::BUCKETING_KEY()->getValue() => $bk, + TreatmentsByFlagSetsArgs::FLAG_SETS()->getValue() => $f, + TreatmentsByFlagSetsArgs::ATTRIBUTES()->getValue() => ($a != null && count($a) > 0) ? $a : null, + ] + ); + } } diff --git a/src/Link/Protocol/V1/SplitViewResult.php b/src/Link/Protocol/V1/SplitViewResult.php index c73c927..3ac839a 100644 --- a/src/Link/Protocol/V1/SplitViewResult.php +++ b/src/Link/Protocol/V1/SplitViewResult.php @@ -11,15 +11,28 @@ class SplitViewResult private /*bool*/ $killed; private /*array*/ $treatments; private /*int*/ $changeNumber; + private /*string*/ $defaultTreatment; + private /*array*/ $sets; private /*array*/ $configs; - public function __construct(string $name, string $trafficType, bool $killed, array $treatments, int $changeNumber, ?array $configs) + public function __construct( + string $name, + string $trafficType, + bool $killed, + array $treatments, + int $changeNumber, + string $defaultTreatment, + array $sets, + ?array $configs + ) { $this->name = $name; $this->trafficType = $trafficType; $this->killed = $killed; $this->treatments = $treatments; $this->changeNumber = $changeNumber; + $this->defaultTreatment = $defaultTreatment; + $this->sets = $sets; $this->configs = $configs; } @@ -52,6 +65,17 @@ public function getConfigs(): ?array { return $this->configs; } + + public function getDefaultTreatment(): string + { + return $this->defaultTreatment; + } + + public function getSets(): array + { + return $this->sets; + } + public static function fromRaw(array $raw): SplitViewResult { return new SplitViewResult( @@ -60,6 +84,8 @@ public static function fromRaw(array $raw): SplitViewResult Enforce::isBool($raw['k']), Enforce::isArray($raw['s']), Enforce::isInt($raw['c']), + Enforce::isString($raw['d']), + isset($raw['e']) ? Enforce::isArray($raw['e']) : [], isset($raw['f']) ? Enforce::isArray($raw['f']) : null ); } diff --git a/src/Link/Protocol/V1/TreatmentsByFlagSetArgs.php b/src/Link/Protocol/V1/TreatmentsByFlagSetArgs.php new file mode 100644 index 0000000..61b0eb6 --- /dev/null +++ b/src/Link/Protocol/V1/TreatmentsByFlagSetArgs.php @@ -0,0 +1,23 @@ +evaluationResults = $results; + } + + public function getEvaluationResults(): array + { + return $this->evaluationResults; + } + + public function getEvaluationResult(int $index): ?EvaluationResult + { + return count($this->evaluationResults) > $index ? $this->evaluationResults[$index] : null; + } + + public static function fromRaw(/*mixed*/$raw)/*: mixed*/ + { + Enforce::isArray($raw); + $status = Result::from(Enforce::isInt($raw['s'])); + + $results = []; + foreach (Enforce::isArray($raw['p']['r']) as $feature => $evalResult) { + $results[$feature] = EvaluationResult::fromRaw(Enforce::isArray($evalResult)); + }; + + return new TreatmentsByFlagSetResponse( + $status, + Enforce::isArray($results) + ); + } +} diff --git a/src/Link/Protocol/V1/TreatmentsByFlagSetsArgs.php b/src/Link/Protocol/V1/TreatmentsByFlagSetsArgs.php new file mode 100644 index 0000000..0fcdede --- /dev/null +++ b/src/Link/Protocol/V1/TreatmentsByFlagSetsArgs.php @@ -0,0 +1,23 @@ +name = $name; $this->trafficType = $trafficType; @@ -19,6 +30,8 @@ public function __construct(string $name, string $trafficType, bool $killed, arr $this->treatments = $treatments; $this->changeNumber = $changeNumber; $this->configs = $configs; + $this->defaultTreatment = $defaultTreatment; + $this->sets = $sets; } public function getName(): string @@ -50,4 +63,14 @@ public function getConfigs(): ?array { return $this->configs; } + + public function getDefaultTreatment(): string + { + return $this->defaultTreatment; + } + + public function getSets(): array + { + return $this->sets; + } } diff --git a/src/Utils/EvalCache/Cache.php b/src/Utils/EvalCache/Cache.php index 5087981..c917b2c 100644 --- a/src/Utils/EvalCache/Cache.php +++ b/src/Utils/EvalCache/Cache.php @@ -8,8 +8,10 @@ public function get(string $key, string $feature, ?array $attributes): ?string; public function getMany(string $key, array $features, ?array $attributes): array; public function getWithConfig(string $key, string $feature, ?array $attributes): ?array; public function getManyWithConfig(string $key, array $features, ?array $attributes): array; + public function getFeaturesByFlagSets(array $flagSets): ?array; public function set(string $key, string $feature, ?array $attributes, string $treatment); public function setMany(string $key, ?array $attributes, array $results); public function setWithConfig(string $key, string $feature, ?array $attributes, string $treatment, ?string $config); public function setManyWithConfig(string $key, ?array $attributes, array $results); + public function setFeaturesForFlagSets(array $flagSets, array $featuresFlags); } diff --git a/src/Utils/EvalCache/CacheImpl.php b/src/Utils/EvalCache/CacheImpl.php index a40c0b6..303b216 100644 --- a/src/Utils/EvalCache/CacheImpl.php +++ b/src/Utils/EvalCache/CacheImpl.php @@ -8,6 +8,7 @@ class CacheImpl implements Cache private /*array*/ $data; private /*InputHasher*/ $hasher; private /*EvictionPolicy*/ $evictionPolicy; + private /*array*/ $flagSets; public function __construct(InputHasher $hasher, EvictionPolicy $evictionPolicy) { @@ -50,6 +51,12 @@ public function getManyWithConfig(string $key, array $features, ?array $attribut return $result; } + public function getFeaturesByFlagSets(array $flagSets): ?array + { + sort($flagSets); // Order flagSets to grab from store + $h = implode(",", $flagSets); // Concatenating each flagSet with a comma e.g: flagSet1,flagSet2,flagSet3 + return $this->flagSets[$h] ?? null; + } public function set(string $key, string $feature, ?array $attributes, string $treatment) { $h = $this->hasher->hashInput($key, $feature, $attributes); @@ -78,6 +85,13 @@ public function setManyWithConfig(string $key, ?array $attributes, array $result } } + public function setFeaturesForFlagSets(array $flagSets, array $featuresFlags) + { + sort($flagSets); // Order flagSets to store + $h = implode(",", $flagSets); // Concatenating each flagSet with a comma e.g: flagSet1,flagSet2,flagSet3 + $this->flagSets[$h] = $featuresFlags; + } + private function _get(string $key, string $feature, ?array $attributes): ?Entry { $h = $this->hasher->hashInput($key, $feature, $attributes); diff --git a/src/Utils/EvalCache/NoCache.php b/src/Utils/EvalCache/NoCache.php index 5132086..bf3637d 100644 --- a/src/Utils/EvalCache/NoCache.php +++ b/src/Utils/EvalCache/NoCache.php @@ -32,6 +32,11 @@ public function getManyWithConfig(string $key, array $features, ?array $attribut return $res; } + public function getFeaturesByFlagSets(array $flagSets): ?array + { + return null; + } + public function set(string $key, string $feature, ?array $attributes, string $treatment) { } @@ -47,4 +52,8 @@ public function setWithConfig(string $key, string $feature, ?array $attributes, public function setManyWithConfig(string $key, ?array $attributes, array $results) { } + + public function setFeaturesForFlagSets(array $flagSets, array $featuresFlags) + { + } } diff --git a/src/Utils/InputValidator/InputValidator.php b/src/Utils/InputValidator/InputValidator.php index 8b2d6eb..5fa62dd 100644 --- a/src/Utils/InputValidator/InputValidator.php +++ b/src/Utils/InputValidator/InputValidator.php @@ -6,6 +6,8 @@ use \Psr\Log\LoggerInterface; +const REG_EXP_FLAG_SET = "/^[a-z0-9][_a-z0-9]{0,49}$/"; + class InputValidator { @@ -39,6 +41,50 @@ public function validProperties(?array $properties): ?array return count($validProperties) > 0 ? $validProperties : null; } + + public function sanitize(string $flagSet, string $method): ?string + { + $trimmed = trim($flagSet); + if ($trimmed !== $flagSet) { + $this->logger->warning($method . ': Flag Set name "' . $flagSet . '" has extra whitespace, trimming.'); + } + $toLowercase = strtolower($trimmed); + if ($toLowercase !== $trimmed) { + $this->logger->warning($method . ': Flag Set name "' . $flagSet . '" should be all lowercase - converting string to lowercase.'); + } + if (!preg_match(REG_EXP_FLAG_SET, $toLowercase)) { + $this->logger->warning($method . ': you passed "' . $flagSet . '", Flag Set must adhere to the regular expressions {' . + REG_EXP_FLAG_SET . '} This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and ' . + 'have a max length of 50 characters. "' . $flagSet . '" was discarded.'); + return null; + } + + return $toLowercase; + } + + public function sanitizeMany(array $flagSets, string $method): ?array + { + if (!array_is_list($flagSets)) { + $this->logger->error($method . ': FlagSets must be a non-empty list.'); + return null; + } + + $sanitized = []; + foreach ($flagSets as $flagSet) { + $sanitizedFlagSet = $this->sanitize($flagSet, $method); + if (!is_null($sanitizedFlagSet)) { + array_push($sanitized, $sanitizedFlagSet); + } + } + $sanitized = array_values(array_unique($sanitized)); + + if (count($sanitized) == 0) { + $this->logger->error($method . ': FlagSets must be a non-empty list.'); + return null; + } + + return $sanitized; + } } // TODO(mredolatti): remove when we deprecate php7 diff --git a/src/Utils/Tracing/Tracer.php b/src/Utils/Tracing/Tracer.php index bf530a5..6b61fe8 100644 --- a/src/Utils/Tracing/Tracer.php +++ b/src/Utils/Tracing/Tracer.php @@ -9,6 +9,10 @@ class Tracer public const METHOD_GET_TREATMENT_WITH_CONFIG = 12; public const METHOD_GET_TREATMENTS_WITH_CONFIG = 13; public const METHOD_TRACK = 14; + public const METHOD_GET_TREATMENTS_BY_FLAG_SET = 15; + public const METHOD_GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET = 16; + public const METHOD_GET_TREATMENTS_BY_FLAG_SETS = 17; + public const METHOD_GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS = 18; public const EVENT_START = 30; public const EVENT_RPC_START = 31; diff --git a/src/Version.php b/src/Version.php index d4ff141..c91c67f 100644 --- a/src/Version.php +++ b/src/Version.php @@ -4,5 +4,5 @@ class Version { - const CURRENT = '1.4.0'; + const CURRENT = '1.5.0'; } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index c6e9838..1eec0a8 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -264,6 +264,202 @@ public function testGetTreatmentsWithConfigAndListener() ); } + public function testGetTreatmentsByFlagSetNoImpListener() { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentsByFlagSet') + ->with('someKey', 'someBuck', 'someset', ['someAttr' => 123]) + ->willReturn([ + 'someFeature1' => ['on', null, null], + 'someFeature2' => ['off', null, null], + 'someFeature3' => ['n/a', null, null], + ]); + + $client = new Client($manager, $this->logger, null); + $this->assertEquals( + ['someFeature1' => 'on', 'someFeature2' => 'off', 'someFeature3' => 'n/a'], + $client->getTreatmentsByFlagSet('someKey', 'someBuck', 'someSet', ['someAttr' => 123]) + ); + } + + public function testGetTreatmentsByFlagSetWithImpListener() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentsByFlagSet') + ->with('someKey', 'someBuck', 'someset', ['someAttr' => 123]) + ->willReturn([ + 'someFeature1' => ['on', new ImpressionListenerData('lab1', 123, 123456), null], + 'someFeature2' => ['off', new ImpressionListenerData('lab1', 124, 123457), null], + 'someFeature3' => ['n/a', new ImpressionListenerData('lab1', 125, 123458), null], + ]); + + $ilMock = $this->createMock(ImpressionListener::class); + $ilMock->expects($this->exactly(3)) + ->method('accept') + ->withConsecutive( + [new Impression('someKey', 'someBuck', 'someFeature1', 'on', 'lab1', 123, 123456), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature2', 'off', 'lab1', 124, 123457), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature3', 'n/a', 'lab1', 125, 123458), ['someAttr' => 123]] + ); + + $client = new Client($manager, $this->logger, $ilMock); + $this->assertEquals( + ['someFeature1' => 'on', 'someFeature2' => 'off', 'someFeature3' => 'n/a'], + $client->getTreatmentsByFlagSet('someKey', 'someBuck', 'someSet', ['someAttr' => 123]) + ); + } + + public function testGetTreatmentsWithConfigByFlagSetAndListener() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentsWithConfigByFlagSet') + ->with('someKey', 'someBuck', 'someset', ['someAttr' => 123]) + ->willReturn([ + 'someFeature1' => ['on', new ImpressionListenerData('lab1', 123, 123456), null], + 'someFeature2' => ['off', new ImpressionListenerData('lab1', 124, 123457), null], + 'someFeature3' => ['n/a', new ImpressionListenerData('lab1', 125, 123458), '{"a": 2}'], + ]); + + $ilMock = $this->createMock(ImpressionListener::class); + $ilMock->expects($this->exactly(3)) + ->method('accept') + ->withConsecutive( + [new Impression('someKey', 'someBuck', 'someFeature1', 'on', 'lab1', 123, 123456), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature2', 'off', 'lab1', 124, 123457), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature3', 'n/a', 'lab1', 125, 123458), ['someAttr' => 123]] + ); + + $client = new Client($manager, $this->logger, $ilMock); + $this->assertEquals( + [ + 'someFeature1' => ['treatment' => 'on', 'config' => null], + 'someFeature2' => ['treatment' => 'off', 'config' => null], + 'someFeature3' => ['treatment' => 'n/a', 'config' => '{"a": 2}'] + ], + $client->getTreatmentsWithConfigByFlagSet('someKey', 'someBuck', 'someSet', ['someAttr' => 123]) + ); + } + + public function testGetTreatmentsByFlagSetsNoImpListener() { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentsByFlagSets') + ->with('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ->willReturn([ + 'someFeature1' => ['on', null, null], + 'someFeature2' => ['off', null, null], + 'someFeature3' => ['n/a', null, null], + ]); + + $client = new Client($manager, $this->logger, null); + $this->assertEquals( + ['someFeature1' => 'on', 'someFeature2' => 'off', 'someFeature3' => 'n/a'], + $client->getTreatmentsByFlagSets('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ); + } + + public function testGetTreatmentsByFlagSetsWithImpListener() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentsByFlagSets') + ->with('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ->willReturn([ + 'someFeature1' => ['on', new ImpressionListenerData('lab1', 123, 123456), null], + 'someFeature2' => ['off', new ImpressionListenerData('lab1', 124, 123457), null], + 'someFeature3' => ['n/a', new ImpressionListenerData('lab1', 125, 123458), null], + ]); + + $ilMock = $this->createMock(ImpressionListener::class); + $ilMock->expects($this->exactly(3)) + ->method('accept') + ->withConsecutive( + [new Impression('someKey', 'someBuck', 'someFeature1', 'on', 'lab1', 123, 123456), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature2', 'off', 'lab1', 124, 123457), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature3', 'n/a', 'lab1', 125, 123458), ['someAttr' => 123]] + ); + + $client = new Client($manager, $this->logger, $ilMock); + $this->assertEquals( + ['someFeature1' => 'on', 'someFeature2' => 'off', 'someFeature3' => 'n/a'], + $client->getTreatmentsByFlagSets('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ); + } + + public function testGetTreatmentsWithConfigByFlagSetsAndListener() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentsWithConfigByFlagSets') + ->with('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ->willReturn([ + 'someFeature1' => ['on', new ImpressionListenerData('lab1', 123, 123456), null], + 'someFeature2' => ['off', new ImpressionListenerData('lab1', 124, 123457), null], + 'someFeature3' => ['n/a', new ImpressionListenerData('lab1', 125, 123458), '{"a": 2}'], + ]); + + $ilMock = $this->createMock(ImpressionListener::class); + $ilMock->expects($this->exactly(3)) + ->method('accept') + ->withConsecutive( + [new Impression('someKey', 'someBuck', 'someFeature1', 'on', 'lab1', 123, 123456), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature2', 'off', 'lab1', 124, 123457), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature3', 'n/a', 'lab1', 125, 123458), ['someAttr' => 123]] + ); + + $client = new Client($manager, $this->logger, $ilMock); + $this->assertEquals( + [ + 'someFeature1' => ['treatment' => 'on', 'config' => null], + 'someFeature2' => ['treatment' => 'off', 'config' => null], + 'someFeature3' => ['treatment' => 'n/a', 'config' => '{"a": 2}'] + ], + $client->getTreatmentsWithConfigByFlagSets('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ); + } + + public function testGetTreatmentsByFlagSetWithEmptyManagerResult() + { + $manager = $this->createMock(Manager::class); + $manager + ->expects($this->once()) + ->method('getTreatmentsByFlagSet') + ->with('someKey', 'someBuck', 'set', ['someAttr' => 123]) + ->willReturn([]); + $manager + ->expects($this->once()) + ->method('getTreatmentsWithConfigByFlagSet') + ->with('someKey', 'someBuck', 'set', ['someAttr' => 123]) + ->willReturn([]); + $cache = $this->createMock(CacheImpl::class); + $cache->expects($this->exactly(2)) + ->method('getFeaturesByFlagSets') + ->with(['set']) + ->willReturn(null); + $client = new Client($manager, $this->logger, null, $cache); + $this->assertEquals([], $client->getTreatmentsByFlagSet('someKey', 'someBuck', 'set', ['someAttr' => 123])); + $this->assertEquals([], $client->getTreatmentsWithConfigByFlagSet('someKey', 'someBuck', 'set', ['someAttr' => 123])); + } + + public function testGetTreatmentsByFlagSetsWithEmptyManagerResult() + { + $manager = $this->createMock(Manager::class); + $manager + ->expects($this->once()) + ->method('getTreatmentsByFlagSets') + ->with('someKey', 'someBuck', ['set'], ['someAttr' => 123]) + ->willReturn([]); + $manager + ->expects($this->once()) + ->method('getTreatmentsWithConfigByFlagSets') + ->with('someKey', 'someBuck', ['set'], ['someAttr' => 123]) + ->willReturn([]); + $cache = $this->createMock(CacheImpl::class); + $cache->expects($this->exactly(2)) + ->method('getFeaturesByFlagSets') + ->with(['set']) + ->willReturn(null); + $client = new Client($manager, $this->logger, null, $cache); + $this->assertEquals([], $client->getTreatmentsByFlagSets('someKey', 'someBuck', ['set'], ['someAttr' => 123])); + $this->assertEquals([], $client->getTreatmentsWithConfigByFlagSets('someKey', 'someBuck', ['set'], ['someAttr' => 123])); + } + public function testGetTreatmentExceptionReturnsControl() { $manager = $this->createMock(Manager::class); @@ -289,6 +485,34 @@ public function testGetTreatmentsExceptionReturnsControl() ); } + public function testGetTreatmentsByFlagSetExceptionReturnsControl() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentsByFlagSet') + ->with('someKey', 'someBuck', 'someset', ['someAttr' => 123]) + ->will($this->throwException(new \Exception("abc"))); + + $client = new Client($manager, $this->logger, null); + $this->assertEquals( + [], + $client->getTreatmentsByFlagSet('someKey', 'someBuck', 'someSet', ['someAttr' => 123]) + ); + } + + public function testGetTreatmentsByFlagSetsExceptionReturnsControl() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentsByFlagSets') + ->with('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ->will($this->throwException(new \Exception("abc"))); + + $client = new Client($manager, $this->logger, null); + $this->assertEquals( + [], + $client->getTreatmentsByFlagSets('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ); + } + public function testGetTreatmentListenerErrorReturnsOk() { $manager = $this->createMock(Manager::class); @@ -452,4 +676,202 @@ public function testGetTreatmentsWithConfigCacheEnabled() ); $this->assertEquals(['f1' => 'on', 'f2' => 'off', 'f3' => 'na'], $client->getTreatments('someKey', 'someBuck', ['f1', 'f2', 'f3'], ['someAttr' => 123])); } + + public function testCacheForGetTreatmentsByFlagSet() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once()) + ->method('getTreatmentsByFlagSet') + ->with('someKey', 'someBuck', 'set_1', ['someAttr' => 123]) + ->willReturn(['f1' => ['on', null, null], 'f2' => ['off', null, null]]); + + $cache = $this->createMock(CacheImpl::class); + $cache->expects($this->exactly(2)) + ->method('getFeaturesByFlagSets') + ->with(['set_1']) + ->willReturnOnConsecutiveCalls(null, ['f1', 'f2']); + $cache->expects($this->once()) + ->method('getMany') + ->with('someKey', ['f1', 'f2'], ['someAttr' => 123]) + ->willReturn(['f1' => 'on', 'f2' => 'off']); + $cache->expects($this->once()) + ->method('setFeaturesForFlagSets') + ->with(['set_1'], ['f1', 'f2']); + $cache->expects($this->once()) + ->method('setMany') + ->with('someKey', ['someAttr' => 123], ['f1' => 'on', 'f2' => 'off']); + + $client = new Client($manager, $this->logger, null, $cache); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatmentsByFlagSet('someKey', 'someBuck', ' set_1 ', ['someAttr' => 123])); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatmentsByFlagSet('someKey', 'someBuck', 'SET_1', ['someAttr' => 123])); + + $manager = $this->createMock(Manager::class); + $manager->expects($this->once()) + ->method('getTreatmentsByFlagSet') + ->with('someKey', 'someBuck', 'set_1', ['someAttr' => 123]) + ->willReturn(['f1' => ['on', null, null], 'f2' => ['off', null, null]]); + $client = new Client($manager, $this->logger, null, new CacheImpl(new KeyAttributeCRC32Hasher(), new NoEviction(0))); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatmentsByFlagSet('someKey', 'someBuck', ' set_1 ', ['someAttr' => 123])); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatmentsByFlagSet('someKey', 'someBuck', 'SET_1', ['someAttr' => 123])); + } + + public function testCacheForGetTreatmentsWithConfigByFlagSet() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once()) + ->method('getTreatmentsWithConfigByFlagSet') + ->with('someKey', 'someBuck', 'set_1', ['someAttr' => 123]) + ->willReturn(['f1' => ['on', null, 'some'], 'f2' => ['off', null, null]]); + + $cache = $this->createMock(CacheImpl::class); + $cache->expects($this->exactly(2)) + ->method('getFeaturesByFlagSets') + ->with(['set_1']) + ->willReturnOnConsecutiveCalls(null, ['f1', 'f2']); + $cache->expects($this->once()) + ->method('setFeaturesForFlagSets') + ->with(['set_1'], ['f1', 'f2']); + $cache->expects($this->once()) + ->method('getManyWithConfig') + ->with('someKey', ['f1', 'f2'], ['someAttr' => 123]) + ->willReturn(['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]]); + $cache->expects($this->once()) + ->method('setManyWithConfig') + ->with( + 'someKey', + ['someAttr' => 123], + [ + 'f1' =>['treatment' => 'on', 'config' => 'some'], + 'f2' => ['treatment' => 'off', 'config' => null] + ]); + + $client = new Client($manager, $this->logger, null, $cache); + $this->assertEquals( + ['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]], + $client->getTreatmentsWithConfigByFlagSet('someKey', 'someBuck', ' set_1 ', ['someAttr' => 123]) + ); + $this->assertEquals( + ['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]], + $client->getTreatmentsWithConfigByFlagSet('someKey', 'someBuck', ' set_1 ', ['someAttr' => 123]) + ); + + $manager = $this->createMock(Manager::class); + $manager->expects($this->once()) + ->method('getTreatmentsWithConfigByFlagSet') + ->with('someKey', 'someBuck', 'set_1', ['someAttr' => 123]) + ->willReturn(['f1' => ['on', null, 'some'], 'f2' => ['off', null, null]]); + $client = new Client($manager, $this->logger, null, new CacheImpl(new KeyAttributeCRC32Hasher(), new NoEviction(0))); + $this->assertEquals( + ['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]], + $client->getTreatmentsWithConfigByFlagSet('someKey', 'someBuck', ' set_1 ', ['someAttr' => 123]) + ); + $this->assertEquals( + ['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]], + $client->getTreatmentsWithConfigByFlagSet('someKey', 'someBuck', ' set_1 ', ['someAttr' => 123]) + ); + } + + public function testCacheForGetTreatmentsByFlagSets() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once()) + ->method('getTreatmentsByFlagSets') + ->with('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ->willReturn(['f1' => ['on', null, null], 'f2' => ['off', null, null]]); + + $cache = $this->createMock(CacheImpl::class); + $cache->expects($this->exactly(2)) + ->method('getFeaturesByFlagSets') + ->with(['set_1', 'set_2']) + ->willReturnOnConsecutiveCalls(null, ['f1', 'f2']); + $cache->expects($this->once()) + ->method('getMany') + ->with('someKey', ['f1', 'f2'], ['someAttr' => 123]) + ->willReturn(['f1' => 'on', 'f2' => 'off']); + $cache->expects($this->once()) + ->method('setFeaturesForFlagSets') + ->with(['set_1', 'set_2'], ['f1', 'f2']); + $cache->expects($this->once()) + ->method('setMany') + ->with('someKey', ['someAttr' => 123], ['f1' => 'on', 'f2' => 'off']); + + $client = new Client($manager, $this->logger, null, $cache); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatmentsByFlagSets('someKey', 'someBuck', [' set_1 ', 'set_2', '@FAIL'], ['someAttr' => 123])); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatmentsByFlagSets('someKey', 'someBuck', ['SET_1', 'set_2', ' '], ['someAttr' => 123])); + + $manager = $this->createMock(Manager::class); + $manager->expects($this->once()) + ->method('getTreatmentsByFlagSets') + ->with('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ->willReturn(['f1' => ['on', null, null], 'f2' => ['off', null, null]]); + $client = new Client($manager, $this->logger, null, new CacheImpl(new KeyAttributeCRC32Hasher(), new NoEviction(0))); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatmentsByFlagSets('someKey', 'someBuck', [' set_1 ', 'set_2', '@FAIL'], ['someAttr' => 123])); + $this->assertEquals(['f1' => 'on', 'f2' => 'off'], $client->getTreatmentsByFlagSets('someKey', 'someBuck', ['SET_1', 'set_2', ' '], ['someAttr' => 123])); + } + + public function testCacheForGetTreatmentsWithConfigByFlagSets() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once()) + ->method('getTreatmentsWithConfigByFlagSets') + ->with('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ->willReturn(['f1' => ['on', null, 'some'], 'f2' => ['off', null, null]]); + + $cache = $this->createMock(CacheImpl::class); + $cache->expects($this->exactly(2)) + ->method('getFeaturesByFlagSets') + ->with(['set_1', 'set_2']) + ->willReturnOnConsecutiveCalls(null, ['f1', 'f2']); + $cache->expects($this->once()) + ->method('setFeaturesForFlagSets') + ->with(['set_1', 'set_2'], ['f1', 'f2']); + $cache->expects($this->once()) + ->method('getManyWithConfig') + ->with('someKey', ['f1', 'f2'], ['someAttr' => 123]) + ->willReturn(['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]]); + $cache->expects($this->once()) + ->method('setManyWithConfig') + ->with( + 'someKey', + ['someAttr' => 123], + [ + 'f1' =>['treatment' => 'on', 'config' => 'some'], + 'f2' => ['treatment' => 'off', 'config' => null] + ]); + + $client = new Client($manager, $this->logger, null, $cache); + $this->assertEquals( + ['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]], + $client->getTreatmentsWithConfigByFlagSets('someKey', 'someBuck', [' set_1 ', 'set_2', '@FAIL'], ['someAttr' => 123]) + ); + $this->assertEquals( + ['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]], + $client->getTreatmentsWithConfigByFlagSets('someKey', 'someBuck', [' set_1 ', 'set_2', ' '], ['someAttr' => 123]) + ); + + $manager = $this->createMock(Manager::class); + $manager->expects($this->once()) + ->method('getTreatmentsWithConfigByFlagSets') + ->with('someKey', 'someBuck', ['set_1', 'set_2'], ['someAttr' => 123]) + ->willReturn(['f1' => ['on', null, 'some'], 'f2' => ['off', null, null]]); + $client = new Client($manager, $this->logger, null, new CacheImpl(new KeyAttributeCRC32Hasher(), new NoEviction(0))); + $this->assertEquals( + ['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]], + $client->getTreatmentsWithConfigByFlagSets('someKey', 'someBuck', [' set_1 ', 'set_2', '@FAIL'], ['someAttr' => 123]) + ); + $this->assertEquals( + ['f1' => ['treatment' => 'on', 'config' => 'some'], 'f2' => ['treatment' => 'off', 'config' => null]], + $client->getTreatmentsWithConfigByFlagSets('someKey', 'someBuck', [' set_1 ', 'set_2', ' '], ['someAttr' => 123]) + ); + } + + public function testInputValidatorForFlagSets() + { + $manager = $this->createMock(Manager::class); + $client = new Client($manager, $this->logger, null, null); + $this->assertEquals([], $client->getTreatmentsByFlagSet('someKey', 'someBuck', '@FAIL', ['someAttr' => 123])); + $this->assertEquals([], $client->getTreatmentsWithConfigByFlagSet('someKey', 'someBuck', '@FAIL', ['someAttr' => 123])); + $this->assertEquals([], $client->getTreatmentsByFlagSets('someKey', 'someBuck', ['@FAIL', ' '], ['someAttr' => 123])); + $this->assertEquals([], $client->getTreatmentsWithConfigByFlagSets('someKey', 'someBuck', ['@FAIL', ' '], ['someAttr' => 123])); + } } diff --git a/tests/Fallback/AwaysControlClientTest.php b/tests/Fallback/AwaysControlClientTest.php index 9989663..4c82b33 100644 --- a/tests/Fallback/AwaysControlClientTest.php +++ b/tests/Fallback/AwaysControlClientTest.php @@ -27,6 +27,20 @@ public function testGetTreatments() $this->assertEquals([], $c->getTreatments("key", null, [], null)); } + public function testGetTreatmentsByFlagSet() + { + $c = new AlwaysControlClient(); + $this->assertEquals([], $c->getTreatmentsByFlagSet("key", null, "someFlagSet", null)); + $this->assertEquals([], $c->getTreatmentsWithConfigByFlagSet("key", null, "someFlagSet", null)); + } + + public function testGetTreatmentsByFlagSets() + { + $c = new AlwaysControlClient(); + $this->assertEquals([], $c->getTreatmentsByFlagSets("key", null, ['set_1', 'set_2'], null)); + $this->assertEquals([], $c->getTreatmentsWithConfigByFlagSets("key", null, ['set_1', 'set_2'], null)); + } + public function testTrack() { $c = new AlwaysControlClient(); diff --git a/tests/Link/Consumer/V1ManagerTest.php b/tests/Link/Consumer/V1ManagerTest.php index 7d5e219..85146de 100644 --- a/tests/Link/Consumer/V1ManagerTest.php +++ b/tests/Link/Consumer/V1ManagerTest.php @@ -30,30 +30,38 @@ public function setUp(): void public function testHappyExchangeNoImpListener(): void { $connMock = $this->createMock(RawConnection::class); - $connMock->expects($this->exactly(5)) + $connMock->expects($this->exactly(9)) ->method('sendMessage') ->withConsecutive( ['serializedRegister'], ['serializedTreatment'], ['serializedTreatments'], ['serializedTreatmentWithConfig'], - ['serializedTreatmentsWithConfig'] + ['serializedTreatmentsWithConfig'], + ['serializedTreatmentsByFlagSet'], + ['serializedTreatmentsWithConfigByFlagSet'], + ['serializedTreatmentsByFlagSets'], + ['serializedTreatmentsWithConfigByFlagSets'] ); - $connMock->expects($this->exactly(5)) + $connMock->expects($this->exactly(9)) ->method('readMessage') ->willReturnOnConsecutiveCalls( 'serializedRegisterResp', 'serializedTreatmentResp', 'serializedTreatmentsResp', - 'serilaizedTreatmentWithCnfigResp', - 'serializedTreatmentsWithConfig' + 'serilaizedTreatmentWithConfigResp', + 'serializedTreatmentsWithConfigResp', + 'serializedTreatmentsByFlagSetResp', + 'serializedTreatmentsWithConfigByFlagSetResp', + 'serializedTreatmentsByFlagSetsResp', + 'serializedTreatmentsWithConfigByFlagSetsResp', ); $connFactoryMock = $this->createMock(ConnectionFactory::class); $connFactoryMock->expects($this->once())->method('create')->willReturn($connMock); $serializerMock = $this->createMock(Serializer::class); - $serializerMock->expects($this->exactly(5)) + $serializerMock->expects($this->exactly(9)) ->method('serialize') ->withConsecutive( [RPC::forRegister('someId', new RegisterFlags(false))], @@ -61,24 +69,46 @@ public function testHappyExchangeNoImpListener(): void [RPC::forTreatments("k", "b", ["f1", "f2", "f3"], ["a" => 1])], [RPC::forTreatmentWithConfig("k", "b", "f", ["a" => 1])], [RPC::forTreatmentsWithConfig("k", "b", ["f1", "f2", "f3"], ["a" => 1])], + [RPC::forTreatmentsByFlagSet("k", "b", "s", ["a" => 1])], + [RPC::forTreatmentsWithConfigByFlagSet("k", "b", "s", ["a" => 1])], + [RPC::forTreatmentsByFlagSets("k", "b", ["s1", "s2"], ["a" => 1])], + [RPC::forTreatmentsWithConfigByFlagSets("k", "b", ["s1", "s2"], ["a" => 1])], ) ->willReturnOnConsecutiveCalls( 'serializedRegister', 'serializedTreatment', 'serializedTreatments', 'serializedTreatmentWithConfig', - 'serializedTreatmentsWithConfig' + 'serializedTreatmentsWithConfig', + 'serializedTreatmentsByFlagSet', + 'serializedTreatmentsWithConfigByFlagSet', + 'serializedTreatmentsByFlagSets', + 'serializedTreatmentsWithConfigByFlagSets' ); - $serializerMock->expects($this->exactly(5)) + $serializerMock->expects($this->exactly(9)) ->method('deserialize') - ->withConsecutive(['serializedRegisterResp'], ['serializedTreatmentResp'], ['serializedTreatmentsResp']) + ->withConsecutive( + ['serializedRegisterResp'], + ['serializedTreatmentResp'], + ['serializedTreatmentsResp'], + ['serilaizedTreatmentWithConfigResp'], + ['serializedTreatmentsWithConfigResp'], + ['serializedTreatmentsByFlagSetResp'], + ['serializedTreatmentsWithConfigByFlagSetResp'], + ['serializedTreatmentsByFlagSetsResp'], + ['serializedTreatmentsWithConfigByFlagSetsResp'] + ) ->willReturnOnConsecutiveCalls( ['s' => 0x01], ['s' => 0x01, 'p' => ['t' => 'on']], ['s' => 0x01, 'p' => ['r' => [['t' => 'on'], ['t' => 'on'], ['t' => 'off']]]], ['s' => 0x01, 'p' => ['t' => 'on', 'c' => '{"a": 1}']], - ['s' => 0x01, 'p' => ['r' => [['t' => 'on'], ['t' => 'on'], ['t' => 'off', 'c' => '{"a": 2}']]]] + ['s' => 0x01, 'p' => ['r' => [['t' => 'on'], ['t' => 'on'], ['t' => 'off', 'c' => '{"a": 2}']]]], + ['s' => 0x01, 'p' => ['r' => ['f1' => ['t' => 'on'], 'f2' => ['t' => 'on'], 'f3' => ['t' => 'off', 'c' => '{"a": 2}']]]], + ['s' => 0x01, 'p' => ['r' => ['f1' => ['t' => 'on'], 'f2' => ['t' => 'on'], 'f3' => ['t' => 'off', 'c' => '{"a": 2}']]]], + ['s' => 0x01, 'p' => ['r' => ['f1' => ['t' => 'on'], 'f2' => ['t' => 'on'], 'f3' => ['t' => 'off', 'c' => '{"a": 2}']]]], + ['s' => 0x01, 'p' => ['r' => ['f1' => ['t' => 'on'], 'f2' => ['t' => 'on'], 'f3' => ['t' => 'off', 'c' => '{"a": 2}']]]] ); $serializerFactoryMock = $this->createMock(SerializerFactory::class); @@ -95,6 +125,22 @@ public function testHappyExchangeNoImpListener(): void ['f1' => ['on', null, null], 'f2' => ['on', null, null], 'f3' => ['off', null, '{"a": 2}']], $v1Manager->getTreatmentsWithConfig('k', 'b', ['f1', 'f2', 'f3'], ['a' => 1]) ); + $this->assertEquals( + ['f1' => ['on', null], 'f2' => ['on', null], 'f3' => ['off', null]], + $v1Manager->getTreatmentsByFlagSet('k', 'b', "s", ['a' => 1]) + ); + $this->assertEquals( + ['f1' => ['on', null, null], 'f2' => ['on', null, null], 'f3' => ['off', null, '{"a": 2}']], + $v1Manager->getTreatmentsWithConfigByFlagSet('k', 'b', "s", ['a' => 1]) + ); + $this->assertEquals( + ['f1' => ['on', null], 'f2' => ['on', null], 'f3' => ['off', null]], + $v1Manager->getTreatmentsByFlagSets('k', 'b', ["s1", "s2"], ['a' => 1]) + ); + $this->assertEquals( + ['f1' => ['on', null, null], 'f2' => ['on', null, null], 'f3' => ['off', null, '{"a": 2}']], + $v1Manager->getTreatmentsWithConfigByFlagSets('k', 'b', ["s1", "s2"], ['a' => 1]) + ); } public function testHappyExchangeWithImpListener(): void @@ -387,6 +433,8 @@ public function testSplit(): void 's' => ['on', 'off'], 'c' => 123, 'f' => ['on' => 'some'], + 'd' => 'on', + 'e' => ['s1', 's2'] ]], ); @@ -395,7 +443,7 @@ public function testSplit(): void $v1Manager = new V1Manager($connFactoryMock, $serializerFactoryMock, Utils::default(), $this->logger); $this->assertEquals( - new SplitView('someName', 'someTrafficType', true, ['on', 'off'], 123, ['on' => 'some']), + new SplitView('someName', 'someTrafficType', true, ['on', 'off'], 123, 'on', ['s1', 's2'], ['on' => 'some']), $v1Manager->split('someName') ); } @@ -434,6 +482,8 @@ public function testSplits(): void 's' => ['on', 'off'], 'c' => 123, 'f' => ['on' => 'some'], + 'd' => 'on', + 'e' => ['s1', 's2'], ], [ 'n' => 'someName2', @@ -442,6 +492,8 @@ public function testSplits(): void 's' => ['on', 'off'], 'c' => 124, 'f' => null, + 'd' => 'off', + 'e' => null, ], ]]], ); @@ -452,8 +504,8 @@ public function testSplits(): void $v1Manager = new V1Manager($connFactoryMock, $serializerFactoryMock, Utils::default(), $this->logger); $this->assertEquals( [ - new SplitView('someName', 'someTrafficType', true, ['on', 'off'], 123, ['on' => 'some']), - new SplitView('someName2', 'someTrafficType', false, ['on', 'off'], 124, null), + new SplitView('someName', 'someTrafficType', true, ['on', 'off'], 123, 'on', ['s1', 's2'], ['on' => 'some']), + new SplitView('someName2', 'someTrafficType', false, ['on', 'off'], 124, 'off', [], null), ], $v1Manager->splits() ); diff --git a/tests/Link/Protocol/V1/RPCTest.php b/tests/Link/Protocol/V1/RPCTest.php index 129561f..1cbcfbf 100644 --- a/tests/Link/Protocol/V1/RPCTest.php +++ b/tests/Link/Protocol/V1/RPCTest.php @@ -2,6 +2,8 @@ namespace SplitIO\Test\Link\Protocol\V1; +use SplitIO\ThinSdk\Link\Protocol\V1\TreatmentsByFlagSetArgs; +use SplitIO\ThinSdk\Link\Protocol\V1\TreatmentsByFlagSetsArgs; use SplitIO\ThinSdk\Link\Protocol\Version; use SplitIO\ThinSdk\Link\Protocol\V1\RPC; use SplitIO\ThinSdk\Link\Protocol\V1\OpCode; @@ -52,4 +54,79 @@ public function testTreatmentsRPC(): void ); } + public function testTreatmentsByFlagSetRPC(): void + { + $dt = new \DateTime('now'); + $rpc = RPC::forTreatmentsByFlagSet('key1', 'buck', 'set', ['a' => 'sarasa', 'b' => 2, 'c' => ['q', 'w'], 'd' => $dt]); + $this->assertEquals(OpCode::TreatmentsByFlagSet(), $rpc->getOpCode()); + $this->assertEquals(Version::V1(), $rpc->getVersion()); + $this->assertEquals('key1', $rpc->getArgs()[TreatmentsByFlagSetArgs::KEY()->getValue()]); + $this->assertEquals('buck', $rpc->getArgs()[TreatmentsByFlagSetArgs::BUCKETING_KEY()->getValue()]); + $this->assertEquals('set', $rpc->getArgs()[TreatmentsByFlagSetArgs::FLAG_SET()->getValue()]); + $this->assertEquals('sarasa', $rpc->getArgs()[TreatmentsByFlagSetArgs::ATTRIBUTES()->getValue()]['a']); + $this->assertEquals(2, $rpc->getArgs()[TreatmentsByFlagSetArgs::ATTRIBUTES()->getValue()]['b']); + $this->assertEquals(['q', 'w'], $rpc->getArgs()[TreatmentsByFlagSetArgs::ATTRIBUTES()->getValue()]['c']); + $this->assertEquals($dt, $rpc->getArgs()[TreatmentsByFlagSetArgs::ATTRIBUTES()->getValue()]['d']); + $this->assertEquals( + ['v' => 1, 'o' => 0x15, 'a' => ['key1', 'buck', 'set', ['a' => 'sarasa', 'b' => 2, 'c' => ['q', 'w'], 'd' => $dt]]], + $rpc->getSerializable() + ); + } + + public function testTreatmentsWithConfigByFlagSetRPC(): void + { + $dt = new \DateTime('now'); + $rpc = RPC::forTreatmentsWithConfigByFlagSet('key1', 'buck', 'set', ['a' => 'sarasa', 'b' => 2, 'c' => ['q', 'w'], 'd' => $dt]); + $this->assertEquals(OpCode::TreatmentsWithConfigByFlagSet(), $rpc->getOpCode()); + $this->assertEquals(Version::V1(), $rpc->getVersion()); + $this->assertEquals('key1', $rpc->getArgs()[TreatmentsByFlagSetArgs::KEY()->getValue()]); + $this->assertEquals('buck', $rpc->getArgs()[TreatmentsByFlagSetArgs::BUCKETING_KEY()->getValue()]); + $this->assertEquals('set', $rpc->getArgs()[TreatmentsByFlagSetArgs::FLAG_SET()->getValue()]); + $this->assertEquals('sarasa', $rpc->getArgs()[TreatmentsByFlagSetArgs::ATTRIBUTES()->getValue()]['a']); + $this->assertEquals(2, $rpc->getArgs()[TreatmentsByFlagSetArgs::ATTRIBUTES()->getValue()]['b']); + $this->assertEquals(['q', 'w'], $rpc->getArgs()[TreatmentsByFlagSetArgs::ATTRIBUTES()->getValue()]['c']); + $this->assertEquals($dt, $rpc->getArgs()[TreatmentsByFlagSetArgs::ATTRIBUTES()->getValue()]['d']); + $this->assertEquals( + ['v' => 1, 'o' => 0x16, 'a' => ['key1', 'buck', 'set', ['a' => 'sarasa', 'b' => 2, 'c' => ['q', 'w'], 'd' => $dt]]], + $rpc->getSerializable() + ); + } + + public function testTreatmentsByFlagSetsRPC(): void + { + $dt = new \DateTime('now'); + $rpc = RPC::forTreatmentsByFlagSets('key1', 'buck', ['set_1', 'set_2'], ['a' => 'sarasa', 'b' => 2, 'c' => ['q', 'w'], 'd' => $dt]); + $this->assertEquals(OpCode::TreatmentsByFlagSets(), $rpc->getOpCode()); + $this->assertEquals(Version::V1(), $rpc->getVersion()); + $this->assertEquals('key1', $rpc->getArgs()[TreatmentsByFlagSetsArgs::KEY()->getValue()]); + $this->assertEquals('buck', $rpc->getArgs()[TreatmentsByFlagSetsArgs::BUCKETING_KEY()->getValue()]); + $this->assertEquals(['set_1', 'set_2'], $rpc->getArgs()[TreatmentsByFlagSetsArgs::FLAG_SETS()->getValue()]); + $this->assertEquals('sarasa', $rpc->getArgs()[TreatmentsByFlagSetsArgs::ATTRIBUTES()->getValue()]['a']); + $this->assertEquals(2, $rpc->getArgs()[TreatmentsByFlagSetsArgs::ATTRIBUTES()->getValue()]['b']); + $this->assertEquals(['q', 'w'], $rpc->getArgs()[TreatmentsByFlagSetsArgs::ATTRIBUTES()->getValue()]['c']); + $this->assertEquals($dt, $rpc->getArgs()[TreatmentsByFlagSetsArgs::ATTRIBUTES()->getValue()]['d']); + $this->assertEquals( + ['v' => 1, 'o' => 0x17, 'a' => ['key1', 'buck', ['set_1', 'set_2'], ['a' => 'sarasa', 'b' => 2, 'c' => ['q', 'w'], 'd' => $dt]]], + $rpc->getSerializable() + ); + } + + public function testTreatmentsWithConfigByFlagSetsRPC(): void + { + $dt = new \DateTime('now'); + $rpc = RPC::forTreatmentsWithConfigByFlagSets('key1', 'buck', ['set_1', 'set_2'], ['a' => 'sarasa', 'b' => 2, 'c' => ['q', 'w'], 'd' => $dt]); + $this->assertEquals(OpCode::TreatmentsWithConfigByFlagSets(), $rpc->getOpCode()); + $this->assertEquals(Version::V1(), $rpc->getVersion()); + $this->assertEquals('key1', $rpc->getArgs()[TreatmentsByFlagSetsArgs::KEY()->getValue()]); + $this->assertEquals('buck', $rpc->getArgs()[TreatmentsByFlagSetsArgs::BUCKETING_KEY()->getValue()]); + $this->assertEquals(['set_1', 'set_2'], $rpc->getArgs()[TreatmentsByFlagSetsArgs::FLAG_SETS()->getValue()]); + $this->assertEquals('sarasa', $rpc->getArgs()[TreatmentsByFlagSetsArgs::ATTRIBUTES()->getValue()]['a']); + $this->assertEquals(2, $rpc->getArgs()[TreatmentsByFlagSetsArgs::ATTRIBUTES()->getValue()]['b']); + $this->assertEquals(['q', 'w'], $rpc->getArgs()[TreatmentsByFlagSetsArgs::ATTRIBUTES()->getValue()]['c']); + $this->assertEquals($dt, $rpc->getArgs()[TreatmentsByFlagSetsArgs::ATTRIBUTES()->getValue()]['d']); + $this->assertEquals( + ['v' => 1, 'o' => 0x18, 'a' => ['key1', 'buck', ['set_1', 'set_2'], ['a' => 'sarasa', 'b' => 2, 'c' => ['q', 'w'], 'd' => $dt]]], + $rpc->getSerializable() + ); + } } diff --git a/tests/Link/Protocol/V1/SplitResponseTest.php b/tests/Link/Protocol/V1/SplitResponseTest.php index 829b148..fdb22f9 100644 --- a/tests/Link/Protocol/V1/SplitResponseTest.php +++ b/tests/Link/Protocol/V1/SplitResponseTest.php @@ -14,17 +14,24 @@ class SplitResponseTest extends TestCase public function testParsingHappyPaths(): void { - $raw = ['s' => 0x01, 'p' => [ 'n' => 'someName', 't' => 'someTrafficType', 'k' => true, 's' => ['on', 'off'], 'c' => 123, + 'd' => 'on', 'f' => ['on' => 'some'], + 'e' => ['s1', 's2'], ]]; $this->assertEquals( - new SplitResponse(Result::Ok(), new SplitViewResult("someName", "someTrafficType", true, ['on', 'off'], 123, ['on' => 'some'])), + new SplitResponse(Result::Ok(), new SplitViewResult("someName", "someTrafficType", true, ['on', 'off'], 123, 'on', ['s1', 's2'], ['on' => 'some'])), + SplitResponse::fromRaw($raw) + ); + + $raw['p']['e'] = []; + $this->assertEquals( + new SplitResponse(Result::Ok(), new SplitViewResult("someName", "someTrafficType", true, ['on', 'off'], 123, 'on', [],['on' => 'some'])), SplitResponse::fromRaw($raw) ); diff --git a/tests/Link/Protocol/V1/SplitsResponseTest.php b/tests/Link/Protocol/V1/SplitsResponseTest.php index fb20ae5..1169fa9 100644 --- a/tests/Link/Protocol/V1/SplitsResponseTest.php +++ b/tests/Link/Protocol/V1/SplitsResponseTest.php @@ -18,14 +18,14 @@ public function testParsingHappyPaths(): void $raw = [ 's' => 0x01, 'p' => ['s' => [ - ['n' => 's1', 't' => 'someTrafficType', 'k' => true, 's' => ['on', 'off'], 'c' => 123, 'f' => ['on' => 'some']], - ['n' => 's2', 't' => 'someTrafficType', 'k' => false, 's' => ['on', 'off'], 'c' => 124, 'f' => null], + ['n' => 's1', 't' => 'someTrafficType', 'k' => true, 's' => ['on', 'off'], 'c' => 123, 'd' => 'on', 'f' => ['on' => 'some'], 'e' => ['s1', 's2']], + ['n' => 's2', 't' => 'someTrafficType', 'k' => false, 's' => ['on', 'off'], 'c' => 124, 'd' => 'on', 'f' => null, 'e' => null], ]] ]; $this->assertEquals( new SplitsResponse(Result::Ok(), [ - new SplitViewResult("s1", "someTrafficType", true, ['on', 'off'], 123, ['on' => 'some']), - new SplitViewResult("s2", "someTrafficType", false, ['on', 'off'], 124, null), + new SplitViewResult("s1", "someTrafficType", true, ['on', 'off'], 123, 'on', ['s1', 's2'], ['on' => 'some']), + new SplitViewResult("s2", "someTrafficType", false, ['on', 'off'], 124, 'on', [], null), ]), SplitsResponse::fromRaw($raw) ); diff --git a/tests/Link/Protocol/V1/TreatmentsByFlagSetResponseTest.php b/tests/Link/Protocol/V1/TreatmentsByFlagSetResponseTest.php new file mode 100644 index 0000000..ba72c1d --- /dev/null +++ b/tests/Link/Protocol/V1/TreatmentsByFlagSetResponseTest.php @@ -0,0 +1,48 @@ + 0x01, 'p' => ['r' => [ + 'f1' => ['t' => 'on', 'l' => ['l' => 'label1', 'c' => 123, 'm' => 456], 'c' => 'cfg'], + 'f2' => ['t' => 'off', 'l' => ['l' => 'label2', 'c' => 124, 'm' => 457]], + 'f3' => ['t' => 'na', 'c' => 'cfg2'], + 'f4' => ['t' => 'pepe'], + ]]]; + + $this->assertEquals( + new TreatmentsByFlagSetResponse(Result::Ok(), [ + 'f1' => new EvaluationResult('on', new ImpressionListenerData('label1', 123, 456), 'cfg'), + 'f2' => new EvaluationResult('off', new ImpressionListenerData('label2', 124, 457), null), + 'f3' => new EvaluationResult('na', null, 'cfg2'), + 'f4' => new EvaluationResult('pepe', null, null), + ]), + TreatmentsByFlagSetResponse::fromRaw($raw) + ); + } + + public function testParsingNonIntStatus(): void + { + $this->expectExceptionMessageMatches("/^expected an int .*/"); + TreatmentsByFlagSetResponse::fromRaw(['s' => 'someStr']); + } + + public function testParsingNonArrayPayload(): void + { + $this->expectExceptionMessageMatches("/^expected an array .*/"); + TreatmentsByFlagSetResponse::fromRaw(['s' => 0x01, 'p' => [ 'r' => true]]); + } +} diff --git a/tests/ManagerTest.php b/tests/ManagerTest.php index 91a8dd9..8772613 100644 --- a/tests/ManagerTest.php +++ b/tests/ManagerTest.php @@ -45,10 +45,10 @@ public function testSplitOk() $lm = $this->createMock(LinkManager::class); $lm->expects($this->once())->method('split') ->with('s1') - ->willReturn(new SplitView('s1', 'tt', true, ['on', 'off'], 123, ['on' => 'frula'])); + ->willReturn(new SplitView('s1', 'tt', true, ['on', 'off'], 123, 'on', ['set_1', 'set_2'], ['on' => 'frula'])); $manager = new Manager($lm, $this->logger); - $this->assertEquals(new SplitView('s1', 'tt', true, ['on', 'off'], 123, ['on' => 'frula']), $manager->split('s1')); + $this->assertEquals(new SplitView('s1', 'tt', true, ['on', 'off'], 123, 'on', ['set_1', 'set_2'], ['on' => 'frula']), $manager->split('s1')); } public function testSplitReturnNullOnException() @@ -68,14 +68,14 @@ public function testSplitsOk() $lm->expects($this->once())->method('splits') ->with() ->willReturn([ - new SplitView('s1', 'tt', true, ['on', 'off'], 123, ['on' => 'frula']), - new SplitView('s2', 'tt', false, ['on', 'off'], 124, ['on' => 'frula']), + new SplitView('s1', 'tt', true, ['on', 'off'], 123, 'on', ['set_1', 'set_2'], ['on' => 'frula']), + new SplitView('s2', 'tt', false, ['on', 'off'], 124, 'off', [], ['on' => 'frula']), ]); $manager = new Manager($lm, $this->logger); $this->assertEquals([ - new SplitView('s1', 'tt', true, ['on', 'off'], 123, ['on' => 'frula']), - new SplitView('s2', 'tt', false, ['on', 'off'], 124, ['on' => 'frula']), + new SplitView('s1', 'tt', true, ['on', 'off'], 123, 'on', ['set_1', 'set_2'], ['on' => 'frula']), + new SplitView('s2', 'tt', false, ['on', 'off'], 124, 'off',[], ['on' => 'frula']), ], $manager->splits()); } diff --git a/tests/Utils/EvalCache/CacheImplTest.php b/tests/Utils/EvalCache/CacheImplTest.php index 8bfdf9f..05888fd 100644 --- a/tests/Utils/EvalCache/CacheImplTest.php +++ b/tests/Utils/EvalCache/CacheImplTest.php @@ -76,4 +76,12 @@ public function testWithConfig() // nothing matches for [] attributes $this->assertEquals(['f1' => null, 'f2' => null, 'f3' => null], $c->getManyWithConfig('key', ['f1', 'f2', 'f3'], [])); } + + public function testByFlagSets() + { + $c = new CacheImpl(new KeyAttributeCRC32Hasher(), new NoEviction(0)); + $c->setFeaturesForFlagSets(['s2', 's1'], ['f1', 'f2']); + $this->assertEquals(['f1', 'f2'], $c->getFeaturesByFlagSets(['s1', 's2'])); + $this->assertEquals(['f1', 'f2'], $c->getFeaturesByFlagSets(['s2', 's1'])); + } } diff --git a/tests/Utils/EvalCache/NoCacheTest.php b/tests/Utils/EvalCache/NoCacheTest.php index a751339..c1ff696 100644 --- a/tests/Utils/EvalCache/NoCacheTest.php +++ b/tests/Utils/EvalCache/NoCacheTest.php @@ -39,5 +39,9 @@ public function testNoCache() $this->assertEquals(null, $c->getWithConfig('key', 'f1', null)); $this->assertEquals(null, $c->getWithConfig('key', 'f2', null)); $this->assertEquals(['f1' => null, 'f2' => null], $c->getManyWithConfig('key', ['f1', 'f2'], null)); + + $c->setFeaturesForFlagSets(['s1', 's2'], ['f1', 'f2']); + $this->assertEquals(null, $c->getFeaturesByFlagSets(['s1', 's2'])); + $this->assertEquals(null, $c->getFeaturesByFlagSets(['s2', 's1'])); } } diff --git a/tests/Utils/InputValidator/InputValidatorTest.php b/tests/Utils/InputValidator/InputValidatorTest.php new file mode 100644 index 0000000..e115aa3 --- /dev/null +++ b/tests/Utils/InputValidator/InputValidatorTest.php @@ -0,0 +1,68 @@ +createMock(\Psr\Log\LoggerInterface::class); + $inputValidator = new InputValidator($logger); + + $logger + ->expects($this->exactly(6)) + ->method('warning') + ->withConsecutive( + ['test: you passed "", Flag Set must adhere to the regular expressions ' . + '{/^[a-z0-9][_a-z0-9]{0,49}$/} This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and ' . + 'have a max length of 50 characters. "" was discarded.'], + ['test: Flag Set name " A" has extra whitespace, trimming.'], + ['test: Flag Set name " A" should be all lowercase - converting string to lowercase.'], + ['test: Flag Set name "A" should be all lowercase - converting string to lowercase.'], + ['test: Flag Set name "@FAIL" should be all lowercase - converting string to lowercase.'], + ['test: you passed "@FAIL", Flag Set must adhere to the regular expressions ' . + '{/^[a-z0-9][_a-z0-9]{0,49}$/} This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and ' . + 'have a max length of 50 characters. "@FAIL" was discarded.'], + ); + + $this->assertEquals(null, $inputValidator->sanitize('', 'test')); + $this->assertEquals('a', $inputValidator->sanitize(' A', 'test')); + $this->assertEquals('a', $inputValidator->sanitize('A', 'test')); + $this->assertEquals(null, $inputValidator->sanitize('@FAIL', 'test')); + } + + public function testSanitizeMany() + { + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $inputValidator = new InputValidator($logger); + + $logger + ->expects($this->exactly(6)) + ->method('warning') + ->withConsecutive( + ['test: Flag Set name " A " has extra whitespace, trimming.'], + ['test: Flag Set name " A " should be all lowercase - converting string to lowercase.'], + ['test: Flag Set name "@FAIL" should be all lowercase - converting string to lowercase.'], + ['test: you passed "@FAIL", Flag Set must adhere to the regular expressions ' . + '{/^[a-z0-9][_a-z0-9]{0,49}$/} This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and ' . + 'have a max length of 50 characters. "@FAIL" was discarded.'], + ['test: Flag Set name "TEST" should be all lowercase - converting string to lowercase.'], + ['test: Flag Set name " a" has extra whitespace, trimming.'], + ); + $logger + ->expects($this->exactly(2)) + ->method('error') + ->withConsecutive( + ['test: FlagSets must be a non-empty list.'], + ['test: FlagSets must be a non-empty list.'] + ); + + $this->assertEquals(['a', 'test'], $inputValidator->sanitizeMany([' A ', '@FAIL', 'TEST'], 'test')); + $this->assertEquals(null, $inputValidator->sanitizeMany([], 'test')); + $this->assertEquals(null, $inputValidator->sanitizeMany(['some' => 'some'], 'test')); + $this->assertEquals(['a', 'test'], $inputValidator->sanitizeMany(['a', 'test', ' a'], 'test')); + } +}