Skip to content

Commit 574669b

Browse files
authored
Feat: Add test command (#55)
1 parent 27ba86e commit 574669b

File tree

4 files changed

+219
-1
lines changed

4 files changed

+219
-1
lines changed

README.md

+37
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,43 @@ unleash_symfony_client:
469469
fetching_enabled: false
470470
```
471471

472+
## Test command
473+
474+
If you need to quickly test what will your flags evaluate to, you can use the built-in command `unleash:test-flag`.
475+
476+
The command is documented and here's the output of `./bin/console unleash:test-flag --help`:
477+
478+
```
479+
Description:
480+
Check the status of an Unleash feature
481+
482+
Usage:
483+
unleash:test-flag [options] [--] <flag>
484+
485+
Arguments:
486+
flag The name of the feature flag to check the result for
487+
488+
Options:
489+
-f, --force When this flag is present, fresh results without cache will be forced
490+
--user-id=USER-ID [Context] Provide the current user's ID
491+
--ip-address=IP-ADDRESS [Context] Provide the current IP address
492+
--session-id=SESSION-ID [Context] Provide the current session ID
493+
--hostname=HOSTNAME [Context] Provide the current hostname
494+
--environment=ENVIRONMENT [Context] Provide the current environment
495+
--current-time=CURRENT-TIME [Context] Provide the current date and time
496+
--custom-context=CUSTOM-CONTEXT [Context] Custom context values in the format [contextName]=[contextValue], for example: myCustomContextField=someValue (multiple values allowed)
497+
--expected=EXPECTED For use in testing, if this option is present, the exit code will be either 0 or 1 depending on whether the expectation matches the result
498+
-h, --help Display help for the given command. When no command is given display help for the list command
499+
-q, --quiet Do not output any message
500+
-V, --version Display this application version
501+
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
502+
-n, --no-interaction Do not ask any interactive question
503+
-e, --env=ENV The Environment name. [default: "dev"]
504+
--no-debug Switch off debug mode.
505+
--profile Enables profiling (requires debug).
506+
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
507+
```
508+
472509
## Configuration reference
473510

474511
This is the autogenerated config dump (by running `php bin/console config:dump unleash_symfony_client`):

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
},
3737
"config": {
3838
"allow-plugins": {
39-
"php-http/discovery": true
39+
"php-http/discovery": true,
40+
"phpstan/extension-installer": false
4041
}
4142
}
4243
}

src/Command/TestFlagCommand.php

+171
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
<?php
2+
3+
namespace Unleash\Client\Bundle\Command;
4+
5+
use LogicException;
6+
use Psr\SimpleCache\CacheInterface;
7+
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Input\InputArgument;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Input\InputOption;
11+
use Symfony\Component\Console\Output\OutputInterface;
12+
use Symfony\Component\Console\Style\SymfonyStyle;
13+
use Unleash\Client\Configuration\Context;
14+
use Unleash\Client\Configuration\UnleashContext;
15+
use Unleash\Client\Unleash;
16+
17+
final class TestFlagCommand extends Command
18+
{
19+
public function __construct(
20+
string $name,
21+
private readonly Unleash $unleash,
22+
private readonly CacheInterface $cache,
23+
) {
24+
parent::__construct($name);
25+
}
26+
27+
protected function configure(): void
28+
{
29+
$this
30+
->setDescription('Check the status of an Unleash feature')
31+
->addArgument(
32+
name: 'flag',
33+
mode: InputArgument::REQUIRED,
34+
description: 'The name of the feature flag to check the result for',
35+
)
36+
->addOption(
37+
name: 'force',
38+
shortcut: 'f',
39+
mode: InputOption::VALUE_NONE,
40+
description: 'When this flag is present, fresh results without cache will be forced',
41+
)
42+
->addOption(
43+
name: 'user-id',
44+
mode: InputOption::VALUE_REQUIRED,
45+
description: "[Context] Provide the current user's ID",
46+
default: null,
47+
)
48+
->addOption(
49+
name: 'ip-address',
50+
mode: InputOption::VALUE_REQUIRED,
51+
description: '[Context] Provide the current IP address',
52+
default: null,
53+
)
54+
->addOption(
55+
name: 'session-id',
56+
mode: InputOption::VALUE_REQUIRED,
57+
description: '[Context] Provide the current session ID',
58+
default: null,
59+
)
60+
->addOption(
61+
name: 'hostname',
62+
mode: InputOption::VALUE_REQUIRED,
63+
description: '[Context] Provide the current hostname',
64+
default: null,
65+
)
66+
->addOption(
67+
name: 'environment',
68+
mode: InputOption::VALUE_REQUIRED,
69+
description: '[Context] Provide the current environment',
70+
default: null,
71+
)
72+
->addOption(
73+
name: 'current-time',
74+
mode: InputOption::VALUE_REQUIRED,
75+
description: '[Context] Provide the current date and time',
76+
default: null,
77+
)
78+
->addOption(
79+
'custom-context',
80+
mode: InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
81+
description: '[Context] Custom context values in the format [contextName]=[contextValue], for example: myCustomContextField=someValue',
82+
default: null,
83+
)
84+
->addOption( // must use positional arguments, because $suggestedValues is not a real argument
85+
'expected',
86+
null,
87+
InputOption::VALUE_REQUIRED,
88+
'For use in testing, if this option is present, the exit code will be either 0 or 1 depending on whether the expectation matches the result',
89+
null,
90+
['true', 'false'], // suggested values
91+
)
92+
;
93+
}
94+
95+
protected function execute(InputInterface $input, OutputInterface $output): int
96+
{
97+
$io = new SymfonyStyle($input, $output);
98+
99+
$flagName = $input->getArgument('flag');
100+
assert(is_string($flagName));
101+
102+
if ($input->getOption('force')) {
103+
$this->cache->clear();
104+
}
105+
106+
$result = $this->unleash->isEnabled(
107+
$flagName,
108+
$this->createContext($input),
109+
);
110+
111+
$expected = $input->getOption('expected');
112+
if ($expected !== null) {
113+
$expected = $expected === 'true';
114+
}
115+
$success = ($expected === null && $result) || ($expected !== null && $result === $expected);
116+
$message = "The feature flag '{$flagName}' evaluated to: " . ($result ? 'true' : 'false');
117+
118+
$success
119+
? $io->success($message)
120+
: $io->error($message)
121+
;
122+
123+
return $expected === null
124+
? Command::SUCCESS
125+
: (
126+
$result === $expected
127+
? Command::SUCCESS
128+
: Command::FAILURE
129+
)
130+
;
131+
}
132+
133+
private function createContext(InputInterface $input): Context
134+
{
135+
$customContextInput = $input->getOption('custom-context');
136+
assert(is_array($customContextInput));
137+
138+
$customContext = [];
139+
foreach ($customContextInput as $item) {
140+
if (!fnmatch('*=*', $item)) {
141+
throw new LogicException('The value must be a key=value pair.');
142+
}
143+
[$key, $value] = explode('=', $item);
144+
$customContext[trim($key)] = trim($value);
145+
}
146+
147+
$userId = $input->getOption('user-id');
148+
$ipAddress = $input->getOption('ip-address');
149+
$sessionId = $input->getOption('session-id');
150+
$hostname = $input->getOption('hostname');
151+
$environment = $input->getOption('environment');
152+
$currentTime = $input->getOption('current-time');
153+
154+
assert($userId === null || is_string($userId));
155+
assert($ipAddress === null || is_string($ipAddress));
156+
assert($sessionId === null || is_string($sessionId));
157+
assert($hostname === null || is_string($hostname));
158+
assert($environment === null || is_string($environment));
159+
assert($currentTime === null || is_string($currentTime));
160+
161+
return new UnleashContext(
162+
currentUserId: $userId,
163+
ipAddress: $ipAddress,
164+
sessionId: $sessionId,
165+
customContext: $customContext,
166+
hostname: $hostname,
167+
environment: $environment,
168+
currentTime: $currentTime,
169+
);
170+
}
171+
}

src/Resources/config/services.yaml

+9
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,12 @@ services:
151151
- '@event_dispatcher'
152152
tags:
153153
- kernel.event_subscriber
154+
155+
unleash.client.command.test_flag:
156+
class: Unleash\Client\Bundle\Command\TestFlagCommand
157+
arguments:
158+
$name: 'unleash:test-flag'
159+
$unleash: '@unleash.client.unleash'
160+
$cache: '@unleash.client.internal.cache'
161+
tags:
162+
- console.command

0 commit comments

Comments
 (0)