diff --git a/.github/workflows/php-ci.yaml b/.github/workflows/php-ci.yaml index 109afb1f..daefd690 100644 --- a/.github/workflows/php-ci.yaml +++ b/.github/workflows/php-ci.yaml @@ -14,13 +14,14 @@ jobs: operating-system: [ubuntu-latest] php-version: ['8.0', '8.1', '8.2'] project-dir: - - hooks/OpenTelemetry - hooks/DDTrace + - hooks/OpenTelemetry - hooks/Validators + # - providers/CloudBees - providers/Flagd - - providers/Split + - providers/Flagsmith - providers/GoFeatureFlag - # - providers/CloudBees + - providers/Split fail-fast: false # todo exclude some matrix combinations based on php version requirements diff --git a/.github/workflows/split_monorepo.yaml b/.github/workflows/split_monorepo.yaml index 56230cf5..f1d4b291 100644 --- a/.github/workflows/split_monorepo.yaml +++ b/.github/workflows/split_monorepo.yaml @@ -24,6 +24,7 @@ jobs: - [hook, validators, Validators] - [provider, cloudbees, CloudBees] - [provider, flagd, Flagd] + - [provider, flagsmith, Flagsmith] - [provider, split, Split] - [provider, go-feature-flag, GoFeatureFlag] steps: diff --git a/providers/Flagsmith/.gitattributes b/providers/Flagsmith/.gitattributes new file mode 100644 index 00000000..a8073ebb --- /dev/null +++ b/providers/Flagsmith/.gitattributes @@ -0,0 +1,15 @@ +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github/ export-ignore +/.gitignore export-ignore +/.readthedocs.yml export-ignore +/build/ export-ignore +/CHANGELOG.md export-ignore +/phpcs.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm-baseline.xml export-ignore +/psalm.xml export-ignore +/tests/ export-ignore + +* text=auto diff --git a/providers/Flagsmith/.gitignore b/providers/Flagsmith/.gitignore new file mode 100644 index 00000000..2e1a6a68 --- /dev/null +++ b/providers/Flagsmith/.gitignore @@ -0,0 +1,3 @@ +/build +/composer.lock +/vendor diff --git a/providers/Flagsmith/CHANGELOG.md b/providers/Flagsmith/CHANGELOG.md new file mode 100644 index 00000000..825c32f0 --- /dev/null +++ b/providers/Flagsmith/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/providers/Flagsmith/README.md b/providers/Flagsmith/README.md new file mode 100644 index 00000000..394997f8 --- /dev/null +++ b/providers/Flagsmith/README.md @@ -0,0 +1,47 @@ +# OpenFeature Flagsmith Provider for PHP + +[![a](https://img.shields.io/badge/slack-%40cncf%2Fopenfeature-brightgreen?style=flat&logo=slack)](https://cloud-native.slack.com/archives/C0344AANLA1) +[![Latest Stable Version](http://poser.pugx.org/open-feature/flagsmith-provider/v)](https://packagist.org/packages/open-feature/flagsmith-provider) +[![Total Downloads](http://poser.pugx.org/open-feature/flagsmith-provider/downloads)](https://packagist.org/packages/open-feature/flagsmith-provider) +![PHP 8.0+](https://img.shields.io/badge/php->=8.0-blue.svg) +[![License](http://poser.pugx.org/open-feature/flagsmith-provider/license)](https://packagist.org/packages/open-feature/flagsmith-provider) + +## Overview + +Flagsmith provides an all-in-one platform for developing, implementing, and managing your feature flags. This repository and package provides the client side code for interacting with it via the OpenFeature PHP SDK. + +This package also builds on various PSRs (PHP Standards Recommendations) such as the Logger interfaces (PSR-3) and the Basic and Extended Coding Standards (PSR-1 and PSR-12). + +## Installation + +```sh +composer require open-feature/flagsmith-provider +``` + +## Usage + +The `FlagsmithProvider` constructor takes a configured Flagsmith client as its only argument: + +```php +$flagsmith = new Flagsmith\Flagsmith('YOUR_FLAGSMITH_API_KEY'); + +OpenFeatureAPI::setProvider(new FlagsmithProvider($flagsmith)); +``` + +## Development + +### PHP Versioning + +This library targets PHP 8.1 and above. As long as you have a compatible version of PHP on your system, you should be able to utilize the OpenFeature SDK. + +This package also has a `.tool-versions` file for use with PHP version managers like `asdf`. + +### Installation and Dependencies + +Install dependencies with `composer install`. + +We value having as few runtime dependencies as possible. The addition of any dependencies requires careful consideration and review. + +### Testing + +Run tests with `composer run test`. diff --git a/providers/Flagsmith/composer.json b/providers/Flagsmith/composer.json new file mode 100644 index 00000000..3d75c93c --- /dev/null +++ b/providers/Flagsmith/composer.json @@ -0,0 +1,133 @@ +{ + "name": "open-feature/flagsmith-provider", + "description": "The Flagsmith provider package for open-feature", + "license": "Apache-2.0", + "type": "library", + "keywords": [ + "featureflags", + "featureflagging", + "openfeature", + "flagsmith", + "provider" + ], + "authors": [ + { + "name": "OpenFeature PHP Maintainers", + "homepage": "https://github.com/orgs/open-feature/teams/php-maintainer" + }, + { + "name": "open-feature/php-sdk-contrib Contributors", + "homepage": "https://github.com/open-feature/php-sdk-contrib/graphs/contributors" + } + ], + "require": { + "php": "^8.1", + "flagsmith/flagsmith-php-client": "^4.5.1", + "open-feature/sdk": "^2.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.25", + "friendsofphp/php-cs-fixer": "^3.13", + "hamcrest/hamcrest-php": "^2.0", + "mdwheele/zalgo": "^0.3.1", + "mockery/mockery": "^1.5", + "phan/phan": "^5.4", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "~1.10.0", + "phpstan/phpstan-mockery": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "psalm/plugin-mockery": "^0.11.0", + "psalm/plugin-phpunit": "^0.18.0", + "ramsey/coding-standard": "^2.0.3", + "ramsey/composer-repl": "^1.4", + "ramsey/conventional-commits": "^1.3", + "roave/security-advisories": "dev-latest", + "spatie/phpunit-snapshot-assertions": "^4.2", + "vimeo/psalm": "~4.30.0" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "autoload": { + "psr-4": { + "OpenFeature\\Providers\\Flagsmith\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "OpenFeature\\Providers\\Flagsmith\\Test\\": "tests/" + } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "ergebnis/composer-normalize": true, + "captainhook/plugin-composer": true, + "ramsey/composer-repl": true, + "php-http/discovery": false + }, + "lock": false, + "sort-packages": true + }, + "extra": { + "captainhook": { + "force-install": false + } + }, + "scripts": { + "dev:analyze": [ + "@dev:analyze:phpstan", + "@dev:analyze:psalm" + ], + "dev:analyze:phpstan": "@php vendor/bin/phpstan analyse --ansi --debug --memory-limit=512M", + "dev:analyze:psalm": "@php vendor/bin/psalm", + "dev:build:clean": "git clean -fX build/", + "dev:grpc": [ + "@dev:grpc:init", + "@dev:grpc:generate", + "@dev:grpc:stage" + ], + "dev:grpc:generate": "export GOPATH=\"$(pwd)/schemas/vendor\" && pushd schemas && make gen-php && popd", + "dev:grpc:init": "git submodule update --recursive", + "dev:grpc:stage": "git add --force ./proto", + "dev:lint": [ + "@dev:lint:syntax", + "@dev:lint:style" + ], + "dev:lint:fix": "@php vendor/bin/phpcbf", + "dev:lint:style": "@php vendor/bin/phpcs --colors", + "dev:lint:syntax": "@php vendor/bin/parallel-lint --colors src/ tests/", + "dev:test": [ + "@dev:lint", + "@dev:analyze", + "@dev:test:unit", + "@dev:test:integration" + ], + "dev:test:coverage:ci": "@php vendor/bin/phpunit --colors=always --coverage-text --coverage-clover build/coverage/clover.xml --coverage-cobertura build/coverage/cobertura.xml --coverage-crap4j build/coverage/crap4j.xml --coverage-xml build/coverage/coverage-xml --log-junit build/junit.xml", + "dev:test:coverage:html": "@php vendor/bin/phpunit --colors=always --coverage-html build/coverage/coverage-html/", + "dev:test:unit": "@php vendor/bin/phpunit --colors=always --testdox", + "dev:test:unit:debug": "@php vendor/bin/phpunit --colors=always --testdox -d xdebug.profiler_enable=on", + "dev:test:unit:setup": "echo 'Setup for unit tests...'", + "dev:test:unit:teardown": "echo 'Tore down unit tests...'", + "dev:test:integration:setup": "echo 'Setup for integration tests...'", + "dev:test:integration:teardown": "echo 'Tore down integration tests...'", + "test": "@dev:test" + }, + "scripts-descriptions": { + "dev:analyze": "Runs all static analysis checks.", + "dev:analyze:phpstan": "Runs the PHPStan static analyzer.", + "dev:analyze:psalm": "Runs the Psalm static analyzer.", + "dev:build:clean": "Cleans the build/ directory.", + "dev:lint": "Runs all linting checks.", + "dev:lint:fix": "Auto-fixes coding standards issues, if possible.", + "dev:lint:style": "Checks for coding standards issues.", + "dev:lint:syntax": "Checks for syntax errors.", + "dev:test": "Runs linting, static analysis, and unit tests.", + "dev:test:coverage:ci": "Runs unit tests and generates CI coverage reports.", + "dev:test:coverage:html": "Runs unit tests and generates HTML coverage report.", + "dev:test:unit": "Runs unit tests.", + "test": "Runs linting, static analysis, and unit tests." + } +} diff --git a/providers/Flagsmith/phpcs.xml.dist b/providers/Flagsmith/phpcs.xml.dist new file mode 100644 index 00000000..55d9d3a1 --- /dev/null +++ b/providers/Flagsmith/phpcs.xml.dist @@ -0,0 +1,25 @@ + + + + + + + + ./src + ./tests + + */tests/fixtures/* + */tests/*/fixtures/* + + + + + + + + + + + + + diff --git a/providers/Flagsmith/phpstan.neon.dist b/providers/Flagsmith/phpstan.neon.dist new file mode 100644 index 00000000..93c5b2d2 --- /dev/null +++ b/providers/Flagsmith/phpstan.neon.dist @@ -0,0 +1,9 @@ +parameters: + tmpDir: ./build/cache/phpstan + level: max + paths: + - ./src + - ./tests + excludePaths: + - */tests/fixtures/* + - */tests/*/fixtures/* diff --git a/providers/Flagsmith/phpunit.xml.dist b/providers/Flagsmith/phpunit.xml.dist new file mode 100644 index 00000000..f96ebda6 --- /dev/null +++ b/providers/Flagsmith/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + + ./tests/Unit + + + + + + ./src + + + + + + + + diff --git a/providers/Flagsmith/psalm-baseline.xml b/providers/Flagsmith/psalm-baseline.xml new file mode 100644 index 00000000..ceaa5778 --- /dev/null +++ b/providers/Flagsmith/psalm-baseline.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/providers/Flagsmith/psalm.xml b/providers/Flagsmith/psalm.xml new file mode 100644 index 00000000..c3e6c03c --- /dev/null +++ b/providers/Flagsmith/psalm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/providers/Flagsmith/src/FlagsmithProvider.php b/providers/Flagsmith/src/FlagsmithProvider.php new file mode 100644 index 00000000..7fb620bf --- /dev/null +++ b/providers/Flagsmith/src/FlagsmithProvider.php @@ -0,0 +1,145 @@ +contextualFlagStore($context)->getFlag($flagKey)->getEnabled(), + ); + } catch (FlagsmithThrowable $throwable) { + return (new ResolutionDetailsBuilder()) + ->withValue($defaultValue) + ->withError( + new ResolutionError(ErrorCode::GENERAL(), $throwable->getMessage()), + )->build(); + } + } + + public function resolveStringValue(string $flagKey, string $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolve($flagKey, $defaultValue, $context); + } + + public function resolveIntegerValue(string $flagKey, int $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolve($flagKey, $defaultValue, $context); + } + + public function resolveFloatValue(string $flagKey, float $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + return $this->resolve($flagKey, $defaultValue, $context); + } + + public function resolveObjectValue(string $flagKey, mixed $defaultValue, ?EvaluationContext $context = null): ResolutionDetails + { + $builder = new ResolutionDetailsBuilder(); + + try { + $value = $this->resolve($flagKey, $defaultValue, $context)->getValue(); + + if ($value !== $defaultValue && is_string($value)) { + /** @var array|bool|DateTime|float|int|string|null $value */ + $value = json_decode($value, true, flags: JSON_THROW_ON_ERROR); + } + + // Valid JSON document might not be an array, so error in this case. + if (!is_array($value) || array_is_list($value)) { + throw new InvalidArgumentException("Flag [$flagKey] value must be a JSON encoded array"); + } + + $builder->withValue($value); + } catch (JsonException | InvalidArgumentException $exception) { + $builder + ->withValue($defaultValue) + ->withError( + new ResolutionError(ErrorCode::PARSE_ERROR(), $exception->getMessage()), + ); + } + + return $builder->build(); + } + + /** + * @param array|bool|DateTime|float|int|string|null $defaultValue + */ + protected function resolve( + string $flagKey, + array | bool | DateTime | float | int | string | null $defaultValue, + ?EvaluationContext $context = null, + ): ResolutionDetails { + $builder = new ResolutionDetailsBuilder(); + + try { + $flag = $this->contextualFlagStore($context)->getFlag($flagKey); + + if ($flag->getEnabled()) { + /** @var array|bool|DateTime|float|int|string|null $value */ + $value = $flag->getValue(); + + $builder->withValue($value); + } else { + $builder + ->withValue($defaultValue) + ->withReason(Reason::DISABLED); + } + } catch (FlagsmithThrowable $throwable) { + $builder + ->withValue($defaultValue) + ->withReason(Reason::ERROR) + ->withError( + new ResolutionError(ErrorCode::GENERAL(), $throwable->getMessage()), + ); + } + + return $builder->build(); + } + + /** + * @throws FlagsmithThrowable + */ + private function contextualFlagStore(?EvaluationContext $context = null): Flags + { + if ($context && !is_null($identifier = $context->getTargetingKey())) { + return $this->flagsmith->getIdentityFlags($identifier, (object) $context->getAttributes()->toArray()); + } + + return $this->flagsmith->getEnvironmentFlags(); + } +} diff --git a/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php b/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php new file mode 100644 index 00000000..c8879607 --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/TestOfflineHandler.php @@ -0,0 +1,21 @@ +environmentModel; + } +} diff --git a/providers/Flagsmith/tests/Fixtures/environments/boolean.json b/providers/Flagsmith/tests/Fixtures/environments/boolean.json new file mode 100644 index 00000000..4f92a246 --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/boolean.json @@ -0,0 +1,67 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": "some-value", + "id": 1, + "feature": { + "name": "some_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": "some-value", + "id": 1, + "feature": { + "name": "disabled_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/Fixtures/environments/float.json b/providers/Flagsmith/tests/Fixtures/environments/float.json new file mode 100644 index 00000000..06b86fb0 --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/float.json @@ -0,0 +1,67 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": 2.345, + "id": 1, + "feature": { + "name": "float_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": -1.0, + "id": 1, + "feature": { + "name": "disabled_float_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/Fixtures/environments/integer.json b/providers/Flagsmith/tests/Fixtures/environments/integer.json new file mode 100644 index 00000000..90d22f1d --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/integer.json @@ -0,0 +1,67 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": 2, + "id": 1, + "feature": { + "name": "integer_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": -1, + "id": 1, + "feature": { + "name": "disabled_integer_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/Fixtures/environments/object.json b/providers/Flagsmith/tests/Fixtures/environments/object.json new file mode 100644 index 00000000..7dcbb52d --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/object.json @@ -0,0 +1,79 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": "{\"key\":\"value\"}", + "id": 1, + "feature": { + "name": "object_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": "some-value", + "id": 1, + "feature": { + "name": "disabled_object_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": "[\"foo\",\"bar\"]", + "id": 1, + "feature": { + "name": "invalid_object_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/Fixtures/environments/string.json b/providers/Flagsmith/tests/Fixtures/environments/string.json new file mode 100644 index 00000000..37259d05 --- /dev/null +++ b/providers/Flagsmith/tests/Fixtures/environments/string.json @@ -0,0 +1,67 @@ +{ + "project": { + "name": "Test project", + "organisation": { + "feature_analytics": false, + "name": "Test Org", + "id": 1, + "persist_trait_data": true, + "stop_serving_flags": false + }, + "id": 1, + "hide_disabled_flags": false + }, + "segment_overrides": [], + "id": 1, + "feature_states": [ + { + "multivariate_feature_state_values": [], + "feature_state_value": "flag value", + "id": 1, + "feature": { + "name": "string_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": true + }, + { + "multivariate_feature_state_values": [], + "feature_state_value": "some-value", + "id": 1, + "feature": { + "name": "disabled_string_feature", + "type": "STANDARD", + "id": 1 + }, + "segment_id": null, + "enabled": false + } + ], + "identity_overrides": [ + { + "identifier": "overridden-id", + "identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01", + "created_date": "2019-08-27T14:53:45.698555Z", + "updated_at": "2023-07-14 16:12:00.000000", + "environment_api_key": "B62qaMZNwfiqT76p38ggrQ", + "identity_features": [ + { + "id": 1, + "feature": { + "id": 1, + "name": "some_feature", + "type": "STANDARD" + }, + "featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f", + "feature_state_value": "some-overridden-value", + "enabled": false, + "environment": 1, + "identity": null, + "feature_segment": null + } + ] + } + ] +} \ No newline at end of file diff --git a/providers/Flagsmith/tests/TestCase.php b/providers/Flagsmith/tests/TestCase.php new file mode 100644 index 00000000..b3e28657 --- /dev/null +++ b/providers/Flagsmith/tests/TestCase.php @@ -0,0 +1,40 @@ + $class + * @param mixed ...$arguments + * + * @return T & MockInterface + * + * @template T + * + * phpcs:disable SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint + */ + public function mockery(string $class, ...$arguments) + { + /** @var T & MockInterface $mock */ + $mock = Mockery::mock($class, ...$arguments); + + return $mock; + } +} diff --git a/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php new file mode 100644 index 00000000..6c140701 --- /dev/null +++ b/providers/Flagsmith/tests/Unit/FlagsmithProviderTest.php @@ -0,0 +1,244 @@ +assertInstanceOf(Provider::class, $provider); + } + + public function testBooleanResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/boolean.json'); + + // When + $resolutionDetails = $provider->resolveBooleanValue('some_feature', false); + + // Then + $this->assertTrue($resolutionDetails->getValue()); + } + + public function testBooleanResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/boolean.json'); + + // When + $resolutionDetails = $provider->resolveBooleanValue('disabled_feature', true); + + // Then + $this->assertFalse($resolutionDetails->getValue()); + } + + public function testBooleanResolutionWithDefaultValueFromFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/boolean.json'); + + // When + $resolutionDetails = $provider->resolveBooleanValue('missing_feature', false); + + // Then + $this->assertFalse($resolutionDetails->getValue()); + $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); + } + + public function testStringResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/string.json'); + + // When + $resolutionDetails = $provider->resolveStringValue('string_feature', 'default value'); + + // Then + $this->assertEquals('flag value', $resolutionDetails->getValue()); + } + + public function testStringResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/string.json'); + + // When + $resolutionDetails = $provider->resolveStringValue('disabled_string_feature', 'default value'); + + // Then + $this->assertEquals('default value', $resolutionDetails->getValue()); + } + + public function testStringResolutionWithMissingFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/string.json'); + + // When + $resolutionDetails = $provider->resolveStringValue('missing_string_feature', 'default value'); + + // Then + $this->assertEquals('default value', $resolutionDetails->getValue()); + $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); + $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason()); + } + + public function testIntegerResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/integer.json'); + + // When + $resolutionDetails = $provider->resolveIntegerValue('integer_feature', 1); + + // Then + $this->assertEquals(2, $resolutionDetails->getValue()); + } + + public function testIntegerResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/integer.json'); + + // When + $resolutionDetails = $provider->resolveIntegerValue('disabled_integer_feature', 456); + + // Then + $this->assertEquals(456, $resolutionDetails->getValue()); + } + + public function testIntegerResolutionWithMissingFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/integer.json'); + + // When + $resolutionDetails = $provider->resolveIntegerValue('missing_integer_feature', 123); + + // Then + $this->assertEquals(123, $resolutionDetails->getValue()); + $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); + $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason()); + } + + public function testFloatResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/float.json'); + + // When + $resolutionDetails = $provider->resolveFloatValue('float_feature', 1.0); + + // Then + $this->assertEquals(2.345, $resolutionDetails->getValue()); + } + + public function testFloatResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/float.json'); + + // When + $resolutionDetails = $provider->resolveFloatValue('disabled_float_feature', 9.99); + + // Then + $this->assertEquals(9.99, $resolutionDetails->getValue()); + } + + public function testFloatResolutionWithMissingFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/float.json'); + + // When + $resolutionDetails = $provider->resolveFloatValue('missing_float_feature', 0.123); + + // Then + $this->assertEquals(0.123, $resolutionDetails->getValue()); + $this->assertEquals(ErrorCode::GENERAL(), $resolutionDetails->getError()?->getResolutionErrorCode()); + $this->assertEquals(Reason::ERROR, $resolutionDetails->getReason()); + } + + public function testObjectResolutionWithEnabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json'); + + // When + $resolutionDetails = $provider->resolveObjectValue('object_feature', ['a' => 'b']); + + // Then + $this->assertEquals(['key' => 'value'], $resolutionDetails->getValue()); + } + + public function testObjectResolutionWithDisabledFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json'); + + // When + $resolutionDetails = $provider->resolveObjectValue('disabled_object_feature', ['a' => 'b']); + + // Then + $this->assertEquals(['a' => 'b'], $resolutionDetails->getValue()); + } + + public function testObjectResolutionWithMissingFlag(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json'); + + // When + $resolutionDetails = $provider->resolveObjectValue('missing_object_feature', ['c' => 3, 'p' => 'o']); + + // Then + $this->assertEquals(['c' => 3, 'p' => 'o'], $resolutionDetails->getValue()); + } + + public function testObjectResolutionWithEnabledFlagWithInvalidValue(): void + { + // Given + $provider = $this->buildProvider(__DIR__ . '/../Fixtures/environments/object.json'); + + // When + $resolutionDetails = $provider->resolveObjectValue('invalid_object_feature', ['default' => 'value']); + + // Then + $this->assertEquals(['default' => 'value'], $resolutionDetails->getValue()); + } +}