Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 897ae8b

Browse files
implement contract test service, not including big segments (#242)
Co-authored-by: Eli Bishop <eli@launchdarkly.com>
1 parent d4ab468 commit 897ae8b

File tree

9 files changed

+279
-2
lines changed

9 files changed

+279
-2
lines changed

.circleci/config.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ jobs:
4141
default: false
4242
docker-image:
4343
type: string
44+
run-contract-tests:
45+
type: boolean
46+
default: true
4447
docker:
4548
- image: <<parameters.docker-image>>
4649
steps:
@@ -56,6 +59,14 @@ jobs:
5659
condition: <<parameters.run-lint>>
5760
steps:
5861
- run: npm run lint
62+
- when:
63+
condition: <<parameters.run-contract-tests>>
64+
steps:
65+
- run:
66+
command: npm run contract-test-service
67+
background: true
68+
- run: mkdir -p reports/junit
69+
- run: TEST_HARNESS_PARAMS="-junit reports/junit/contract-test-results.xml -skip-from contract-tests/testharness-suppressions.txt" npm run contract-test-harness
5970
- run:
6071
name: dependency audit
6172
command: ./scripts/better-audit.sh

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package-lock.json
22
/docs/build/
33
/local/
4-
/node_modules/
4+
**/node_modules/
55
junit.xml
66
npm-debug.log
77
test-types.js

contract-tests/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 `npm run contract-tests` from the SDK project root directory. This will start the test service, download the correct version of the test harness tool, and run the tests.
6+
7+
Or, to test against an in-progress local version of the test harness, run `npm run 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.

contract-tests/index.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const express = require('express');
2+
const bodyParser = require('body-parser');
3+
4+
const { Log } = require('./log');
5+
const { newSdkClientEntity, badCommandError } = require('./sdkClientEntity');
6+
7+
const app = express();
8+
let server = null;
9+
10+
const port = 8000;
11+
12+
let clientCounter = 0;
13+
const clients = {};
14+
15+
const mainLog = Log('service');
16+
17+
app.use(bodyParser.json());
18+
19+
app.get('/', (req, res) => {
20+
res.header('Content-Type', 'application/json');
21+
res.json({
22+
capabilities: [
23+
'server-side',
24+
'all-flags-client-side-only',
25+
'all-flags-details-only-for-tracked-flags',
26+
'all-flags-with-reasons',
27+
],
28+
});
29+
});
30+
31+
app.delete('/', (req, res) => {
32+
mainLog.info('Test service has told us to exit');
33+
res.status(204);
34+
res.send();
35+
36+
// Defer the following actions till after the response has been sent
37+
setTimeout(() => {
38+
server.close(() => process.exit());
39+
// We force-quit with process.exit because, even after closing the server, there could be some
40+
// scheduled tasks lingering if an SDK instance didn't get cleaned up properly, and we don't want
41+
// that to prevent us from quitting.
42+
}, 1);
43+
});
44+
45+
app.post('/', async (req, res) => {
46+
const options = req.body;
47+
48+
clientCounter += 1;
49+
const clientId = clientCounter.toString();
50+
const resourceUrl = `/clients/${clientId}`;
51+
52+
try {
53+
const client = await newSdkClientEntity(options);
54+
clients[clientId] = client;
55+
56+
res.status(201);
57+
res.set('Location', resourceUrl);
58+
} catch (e) {
59+
res.status(500);
60+
const message = e.message || JSON.stringify(e);
61+
mainLog.error('Error creating client: ' + message);
62+
res.write(message);
63+
}
64+
res.send();
65+
});
66+
67+
app.post('/clients/:id', async (req, res) => {
68+
const client = clients[req.params.id];
69+
if (!client) {
70+
res.status(404);
71+
} else {
72+
try {
73+
const respValue = await client.doCommand(req.body);
74+
if (respValue) {
75+
res.status(200);
76+
res.write(JSON.stringify(respValue));
77+
} else {
78+
res.status(204);
79+
}
80+
} catch (e) {
81+
const isBadRequest = e === badCommandError;
82+
res.status(isBadRequest ? 400 : 500);
83+
res.write(e.message || JSON.stringify(e));
84+
if (!isBadRequest && e.stack) {
85+
console.log(e.stack);
86+
}
87+
}
88+
}
89+
res.send();
90+
});
91+
92+
app.delete('/clients/:id', async (req, res) => {
93+
const client = clients[req.params.id];
94+
if (!client) {
95+
res.status(404);
96+
res.send();
97+
} else {
98+
client.close();
99+
delete clients[req.params.id];
100+
res.status(204);
101+
res.send();
102+
}
103+
});
104+
105+
server = app.listen(port, () => {
106+
console.log('Listening on port %d', port);
107+
});

contract-tests/log.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const ld = require('launchdarkly-node-server-sdk');
2+
3+
function Log(tag) {
4+
function doLog(level, message) {
5+
console.log(new Date().toISOString() + ` [${tag}] ${level}: ${message}`);
6+
}
7+
return {
8+
info: message => doLog('info', message),
9+
error: message => doLog('error', message),
10+
};
11+
}
12+
13+
function sdkLogger(tag) {
14+
return ld.basicLogger({
15+
level: 'debug',
16+
destination: line => {
17+
console.log(new Date().toISOString() + ` [${tag}.sdk] ${line}`);
18+
},
19+
});
20+
}
21+
22+
module.exports.Log = Log;
23+
module.exports.sdkLogger = sdkLogger;

contract-tests/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "node-server-sdk-contract-tests",
3+
"version": "0.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"start": "node index.js"
7+
},
8+
"author": "",
9+
"license": "Apache-2.0",
10+
"dependencies": {
11+
"body-parser": "^1.19.0",
12+
"express": "^4.17.1",
13+
"launchdarkly-node-server-sdk": "file:.."
14+
}
15+
}

contract-tests/sdkClientEntity.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const ld = require('launchdarkly-node-server-sdk');
2+
3+
const { Log, sdkLogger } = require('./log');
4+
5+
const badCommandError = new Error('unsupported command');
6+
7+
function makeSdkConfig(options, tag) {
8+
const cf = {
9+
logger: sdkLogger(tag),
10+
};
11+
const maybeTime = seconds => (seconds === undefined || seconds === null ? undefined : seconds / 1000);
12+
if (options.streaming) {
13+
cf.streamUri = options.streaming.baseUri;
14+
cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs);
15+
}
16+
if (options.events) {
17+
cf.allAttributesPrivate = options.events.allAttributesPrivate;
18+
cf.eventsUri = options.events.baseUri;
19+
cf.capacity = options.events.capacity;
20+
cf.diagnosticOptOut = !options.events.enableDiagnostics;
21+
cf.flushInterval = maybeTime(options.events.flushIntervalMs);
22+
cf.inlineUsersInEvents = options.events.inlineUsers;
23+
cf.privateAttributeNames = options.events.globalPrivateAttributes;
24+
}
25+
return cf;
26+
}
27+
28+
async function newSdkClientEntity(options) {
29+
const c = {};
30+
const log = Log(options.tag);
31+
32+
log.info('Creating client with configuration: ' + JSON.stringify(options.configuration));
33+
const timeout =
34+
options.configuration.startWaitTimeMs !== null && options.configuration.startWaitTimeMs !== undefined
35+
? options.configuration.startWaitTimeMs
36+
: 5000;
37+
const client = ld.init(
38+
options.configuration.credential || 'unknown-sdk-key',
39+
makeSdkConfig(options.configuration, options.tag)
40+
);
41+
try {
42+
await Promise.race([client.waitForInitialization(), new Promise(resolve => setTimeout(resolve, timeout))]);
43+
} catch (_) {
44+
// if waitForInitialization() rejects, the client failed to initialize, see next line
45+
}
46+
if (!client.initialized() && !options.configuration.initCanFail) {
47+
client.close();
48+
throw new Error('client initialization failed');
49+
}
50+
51+
c.close = () => {
52+
client.close();
53+
log.info('Test ended');
54+
};
55+
56+
c.doCommand = async params => {
57+
log.info('Received command: ' + params.command);
58+
switch (params.command) {
59+
case 'evaluate': {
60+
const pe = params.evaluate;
61+
if (pe.detail) {
62+
return await client.variationDetail(pe.flagKey, pe.user, pe.defaultValue);
63+
} else {
64+
const value = await client.variation(pe.flagKey, pe.user, pe.defaultValue);
65+
return { value };
66+
}
67+
}
68+
69+
case 'evaluateAll': {
70+
const pea = params.evaluateAll;
71+
const eao = {
72+
clientSideOnly: pea.clientSideOnly,
73+
detailsOnlyForTrackedFlags: pea.detailsOnlyForTrackedFlags,
74+
withReasons: pea.withReasons,
75+
};
76+
return { state: await client.allFlagsState(pea.user, eao) };
77+
}
78+
79+
case 'identifyEvent':
80+
client.identify(params.identifyEvent.user);
81+
return undefined;
82+
83+
case 'customEvent': {
84+
const pce = params.customEvent;
85+
client.track(pce.eventKey, pce.user, pce.data, pce.metricValue);
86+
return undefined;
87+
}
88+
89+
case 'aliasEvent':
90+
client.alias(params.aliasEvent.user, params.aliasEvent.previousUser);
91+
return undefined;
92+
93+
case 'flushEvents':
94+
client.flush();
95+
return undefined;
96+
97+
case 'getBigSegmentStoreStatus':
98+
return undefined;
99+
100+
default:
101+
throw badCommandError;
102+
}
103+
};
104+
105+
return c;
106+
}
107+
108+
module.exports.newSdkClientEntity = newSdkClientEntity;
109+
module.exports.badCommandError = badCommandError;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
streaming/validation/drop and reconnect if stream event has malformed JSON
2+
streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
"scripts": {
77
"test": "jest --ci",
88
"check-typescript": "node_modules/typescript/bin/tsc",
9-
"lint": "eslint --format 'node_modules/eslint-formatter-pretty' --ignore-path .eslintignore ."
9+
"lint": "eslint --format 'node_modules/eslint-formatter-pretty' --ignore-path .eslintignore .",
10+
"contract-test-service": "npm --prefix contract-tests install && npm --prefix contract-tests start",
11+
"contract-test-harness": "curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/master/downloader/run.sh \\ | VERSION=v1 PARAMS=\"-url http://localhost:8000 -debug -stop-service-at-end $TEST_HARNESS_PARAMS\" sh",
12+
"contract-tests": "npm run contract-test-service & npm run contract-test-harness"
1013
},
1114
"types": "./index.d.ts",
1215
"repository": {

0 commit comments

Comments
 (0)