Skip to content

Commit 113969e

Browse files
authored
implement contract test service (#95)
1 parent eddb393 commit 113969e

File tree

11 files changed

+600
-0
lines changed

11 files changed

+600
-0
lines changed

CONTRIBUTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,9 @@ By default, this test suite does not include any integration test that relies on
3939
```
4040
docker run --rm -p 8080:8080 wiremock/wiremock
4141
```
42+
43+
To run the SDK contract test suite in Linux (see [`test-service/README.md`](./test-service/README.md)):
44+
45+
```bash
46+
make contract-tests
47+
```

Dockerfile.testservice

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# syntax=docker/dockerfile:experimental
2+
3+
FROM ubuntu:18.04
4+
5+
ENV DEBIAN_FRONTEND=noninteractive
6+
7+
RUN apt update && apt install -y software-properties-common && \
8+
add-apt-repository ppa:ondrej/php && apt update && \
9+
apt install -y \
10+
php7.3 \
11+
curl \
12+
unzip \
13+
apache2 \
14+
git
15+
16+
# php7.3 includes libapache-mod-php7.3
17+
18+
RUN a2enmod rewrite
19+
20+
# Send error to stdout
21+
RUN ln -sf /proc/self/fd/1 /var/log/apache2/access.log && \
22+
ln -sf /proc/self/fd/1 /var/log/apache2/error.log
23+
24+
RUN mkdir ~/.ssh && \
25+
ssh-keyscan github.com >> ~/.ssh/known_hosts && \
26+
git config --global url."git@github.com:launchdarkly/".insteadOf \
27+
https://github.com/launchdarkly/
28+
29+
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
30+
31+
WORKDIR /app
32+
33+
COPY . .
34+
35+
COPY test-service/apache2.conf /etc/apache2/apache2.conf
36+
37+
RUN cd /app/test-service && mkdir data-store && chown www-data:www-data data-store
38+
39+
RUN cd /app/test-service && composer install --no-progress
40+
41+
CMD apache2ctl -D FOREGROUND

Makefile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
TEMP_TEST_OUTPUT=/tmp/sse-contract-test-service.log
3+
4+
# TEST_HARNESS_PARAMS can be set to add -skip parameters for any contract tests that cannot yet pass
5+
TEST_HARNESS_PARAMS=
6+
7+
build-contract-tests:
8+
@docker build -f Dockerfile.testservice -t php-test-service .
9+
10+
start-contract-test-service: build-contract-tests
11+
@docker run -it --rm \
12+
--name php-test-service \
13+
--publish 8000:8000 \
14+
--add-host=host.docker.internal:host-gateway \
15+
php-test-service
16+
17+
start-contract-test-service-bg:
18+
@echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)"
19+
@make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 &
20+
21+
run-contract-tests:
22+
@curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/main/downloader/run.sh \
23+
| VERSION=v1 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end $(TEST_HARNESS_PARAMS)" sh
24+
25+
contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests
26+
27+
.PHONY: build-contract-tests start-contract-test-service start-contract-test-service-bg run-contract-tests contract-tests

test-service/.htaccess

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RewriteEngine On
2+
RewriteCond %{REQUEST_FILENAME} !-f
3+
RewriteCond %{REQUEST_FILENAME} !-d
4+
RewriteRule ^(.*)$ index.php [QSA,L]

test-service/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SDK contract test service
2+
3+
This directory contains an implementation of the cross-platform SDK testing protocol defined by https://github.com/launchdarkly/sdk-test-harness. See that project's `README` for details of this protocol, and the kinds of SDK capabilities that are relevant to the contract tests. This code should not need to be updated unless the SDK has added or removed such capabilities.
4+
5+
To run these tests locally, run `make contract-tests` from the SDK project root directory. This downloads the correct version of the test harness tool automatically.
6+
7+
Or, to test against an in-progress local version of the test harness, run `make start-contract-test-service` from the SDK project root directory; then, in the root directory of the `sdk-test-harness` project, build the test harness and run it from the command line.

test-service/SdkClientEntity.php

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
<?php
2+
3+
class SdkClientEntity
4+
{
5+
private $_client;
6+
private $_logger;
7+
8+
public function __construct($params)
9+
{
10+
$tag = $params['tag'];
11+
12+
$logger = new Monolog\Logger('sdkclient');
13+
$stream = new Monolog\Handler\StreamHandler('php://stderr', Monolog\Logger::DEBUG);
14+
$stream->setFormatter(new Monolog\Formatter\LineFormatter(
15+
"[%datetime%] %channel%.%level_name%: [$tag] %message%\n"
16+
));
17+
$logger->pushHandler($stream);
18+
$this->_logger = $logger;
19+
20+
$this->_client = self::createSdkClient($params, $logger);
21+
}
22+
23+
public static function createSdkClient($params, $logger)
24+
{
25+
$config = $params['configuration'];
26+
27+
$sdkKey = $config['credential'];
28+
$options = [
29+
'event_publisher' => LaunchDarkly\Integrations\Guzzle::eventPublisher(),
30+
'logger' => $logger
31+
];
32+
33+
$pollingConfig = $config['polling'] ?? [];
34+
$options['base_uri'] = self::adjustBaseUri($pollingConfig['baseUri'] ?? null);
35+
36+
$options['send_events'] = ($config['events'] ?? null) !== null;
37+
$eventsConfig = $config['events'] ?? [];
38+
$options['events_uri'] = self::adjustBaseUri($eventsConfig['baseUri'] ?? null);
39+
$options['all_attributes_private'] = $eventsConfig['allAttributesPrivate'] ?? false;
40+
$options['private_attribute_names'] = $eventsConfig['globalPrivateAttributes'] ?? null;
41+
42+
return new LaunchDarkly\LDClient($sdkKey, $options);
43+
}
44+
45+
public function close()
46+
{
47+
// there isn't really any cleanup to do
48+
$this->_logger->info('Test ended');
49+
}
50+
51+
public function doCommand($reqParams)
52+
{
53+
$command = $reqParams['command'];
54+
$commandParams = $reqParams[$command] ?? null;
55+
switch ($command) {
56+
case 'customEvent':
57+
$this->doCustomEvent($commandParams);
58+
return null;
59+
60+
case 'evaluate':
61+
return $this->doEvaluate($commandParams);
62+
63+
case 'evaluateAll':
64+
return $this->doEvaluateAll($commandParams);
65+
66+
case 'identifyEvent':
67+
$this->doIdentifyEvent($commandParams);
68+
return null;
69+
70+
case 'flushEvents':
71+
$this->_client->flush();
72+
return null;
73+
74+
case 'secureModeHash':
75+
return $this->doSecureModeHash($commandParams);
76+
77+
default:
78+
return false; // means invalid command
79+
}
80+
}
81+
82+
private static function adjustBaseUri($baseUri)
83+
{
84+
return $baseUri ?
85+
preg_replace('@^http://localhost:@', 'http://host.docker.internal:', $baseUri)
86+
: null;
87+
}
88+
89+
private function doCustomEvent($params)
90+
{
91+
$this->_client->track(
92+
$params['eventKey'],
93+
$this->makeUser($params['user']),
94+
$params['data'] ?? null,
95+
$params['metricValue'] ?? null
96+
);
97+
}
98+
99+
private function doEvaluate($params)
100+
{
101+
$flagKey = $params['flagKey'];
102+
$user = $this->makeUser($params['user']);
103+
$defaultValue = $params['defaultValue'] ?? null;
104+
$detail = $params['detail'] ?? false;
105+
106+
if ($detail) {
107+
$result = $this->_client->variationDetail($flagKey, $user, $defaultValue);
108+
return [
109+
"value" => $result->getValue(),
110+
"variationIndex" => $result->getVariationIndex(),
111+
"reason" => $result->getReason()
112+
];
113+
} else {
114+
$value = $this->_client->variation($flagKey, $user, $defaultValue);
115+
return [
116+
"value" => $value
117+
];
118+
}
119+
}
120+
121+
private function doEvaluateAll($params)
122+
{
123+
$options = [];
124+
foreach (['clientSideOnly', 'detailsOnlyForTrackedFlags', 'withReasons'] as $option) {
125+
if ($params[$option] ?: false) {
126+
$options[$option] = true;
127+
}
128+
}
129+
$state = $this->_client->allFlagsState($this->makeUser($params['user']), $options);
130+
return [
131+
'state' => $state->jsonSerialize()
132+
];
133+
}
134+
135+
private function doIdentifyEvent($params)
136+
{
137+
$this->_client->identify($this->makeUser($params['user']));
138+
}
139+
140+
private function doSecureModeHash($params)
141+
{
142+
$user = $this->makeUser($params['user']);
143+
$result = $this->_client->secureModeHash($user);
144+
return [
145+
'result' => $result
146+
];
147+
}
148+
149+
private function makeUser($data)
150+
{
151+
$privateAttributeNames = $data['privateAttributeNames'] ?? [];
152+
153+
$builder = new LaunchDarkly\LDUserBuilder(isset($data['key']) ? $data['key'] : null);
154+
155+
$secondary = $data['secondary'] ?? null;
156+
if (in_array('secondary', $privateAttributeNames)) {
157+
$builder->privateSecondary($secondary);
158+
} else {
159+
$builder->secondary($secondary);
160+
}
161+
162+
$ip = $data['ip'] ?? null;
163+
if (in_array('ip', $privateAttributeNames)) {
164+
$builder->privateIp($ip);
165+
} else {
166+
$builder->ip($ip);
167+
}
168+
169+
$country = $data['country'] ?? null;
170+
if (in_array('country', $privateAttributeNames)) {
171+
$builder->privateCountry($country);
172+
} else {
173+
$builder->country($country);
174+
}
175+
176+
$email = $data['email'] ?? null;
177+
if (in_array('email', $privateAttributeNames)) {
178+
$builder->privateEmail($email);
179+
} else {
180+
$builder->email($email);
181+
}
182+
183+
$name = $data['name'] ?? null;
184+
if (in_array('name', $privateAttributeNames)) {
185+
$builder->privateName($name);
186+
} else {
187+
$builder->name($name);
188+
}
189+
190+
$avatar = $data['avatar'] ?? null;
191+
if (in_array('avatar', $privateAttributeNames)) {
192+
$builder->privateAvatar($avatar);
193+
} else {
194+
$builder->avatar($avatar);
195+
}
196+
197+
$firstName = $data['firstName'] ?? null;
198+
if (in_array('firstName', $privateAttributeNames)) {
199+
$builder->privateFirstName($firstName);
200+
} else {
201+
$builder->firstName($firstName);
202+
}
203+
204+
$lastName = $data['lastName'] ?? null;
205+
if (in_array('lastName', $privateAttributeNames)) {
206+
$builder->privateLastName($lastName);
207+
} else {
208+
$builder->lastName($lastName);
209+
}
210+
211+
if (isset($data['anonymous'])) {
212+
$builder->anonymous($data['anonymous']);
213+
}
214+
215+
if (isset($data['custom'])) {
216+
foreach ($data['custom'] as $key => $value) {
217+
if (in_array($key, $privateAttributeNames)) {
218+
$builder->privateCustomAttribute($key, $value);
219+
} else {
220+
$builder->customAttribute($key, $value);
221+
}
222+
}
223+
}
224+
225+
return $builder->build();
226+
}
227+
}

test-service/TestDataStore.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
class TestDataStore
4+
{
5+
private $_basePath;
6+
7+
private const PREFIX = "client-params-";
8+
9+
public function __construct($basePath)
10+
{
11+
$this->_basePath = $basePath;
12+
}
13+
14+
public function addClientParams($params)
15+
{
16+
$data = json_encode($params);
17+
18+
// call tempnam() to pick a random filename that doesn't already exist in our directory,
19+
// and use that filename as the client ID from now on
20+
$filePath = tempnam($this->_basePath, self::PREFIX);
21+
file_put_contents($filePath, $data);
22+
$id = substr_replace(basename($filePath), "", 0, strlen(self::PREFIX));
23+
24+
return $id;
25+
}
26+
27+
public function getClientParams($id)
28+
{
29+
$data = file_get_contents($this->getClientParamsFilePath($id));
30+
if ($data === false) {
31+
return null;
32+
}
33+
return json_decode($data, true);
34+
}
35+
36+
public function deleteClientParams($id)
37+
{
38+
unlink($this->getClientParamsFilePath($id));
39+
}
40+
41+
private function getClientParamsFilePath($id)
42+
{
43+
return $this->_basePath . '/' . self::PREFIX . $id;
44+
}
45+
}

0 commit comments

Comments
 (0)