Skip to content

Commit 582500e

Browse files
committed
chore(node-client-sdk): Add contract tests and hello example
1 parent 6091a47 commit 582500e

17 files changed

Lines changed: 831 additions & 4 deletions

File tree

.github/workflows/node-client.yml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,19 @@ jobs:
3131
with:
3232
workspace_name: '@launchdarkly/node-client-sdk'
3333
workspace_path: packages/sdk/node-client
34-
# TODO: Add contract tests
34+
- name: Install contract test service dependencies
35+
env:
36+
ELECTRON_SKIP_BINARY_DOWNLOAD: '1'
37+
run: yarn workspace @launchdarkly/node-client-sdk-contract-tests install --no-immutable
38+
- name: Build shared contract test utils
39+
run: yarn workspace @launchdarkly/js-contract-test-utils build:no-server
40+
- name: Build the test service
41+
run: yarn workspace @launchdarkly/node-client-sdk-contract-tests build
42+
- name: Launch the test service in the background
43+
run: yarn workspace @launchdarkly/node-client-sdk-contract-tests start 2>&1 &
44+
- name: Run contract tests (FDv1)
45+
uses: launchdarkly/gh-actions/actions/contract-tests@5adb11fd6953e1bc35d9cf1fc1b4374c464e3a8b # contract-tests-v1.3.0
46+
with:
47+
test_service_port: 8000
48+
token: ${{ secrets.GITHUB_TOKEN }}
49+
extra_params: '--skip-from=${{ github.workspace }}/packages/sdk/node-client/contract-tests/testharness-suppressions.txt'

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
"packages/sdk/electron/example",
1919
"packages/sdk/electron/contract-tests/entity",
2020
"packages/sdk/node-client",
21+
"packages/sdk/node-client/examples/hello-node-client",
22+
"packages/sdk/node-client/contract-tests",
2123
"packages/sdk/fastly",
2224
"packages/sdk/fastly/example",
2325
"packages/sdk/react",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Node Client SDK Contract Tests
2+
3+
This directory contains the contract test implementation for the LaunchDarkly Client-Side SDK for Node.js using the [SDK Test Harness](https://github.com/launchdarkly/sdk-test-harness).
4+
5+
The contract test service is an Express server that exposes a REST API on port 8000. The test harness sends commands to this service, which creates and manages SDK client instances and executes flag evaluations, events, and other operations.
6+
7+
## Running locally
8+
9+
From the SDK package directory (`packages/sdk/node-client`):
10+
11+
```bash
12+
yarn contract-tests
13+
```
14+
15+
This builds the SDK and the contract-test service, starts the service in the background on port 8000, downloads the matching `sdk-test-harness` binary, and runs the harness against the service. The harness shuts the service down when it finishes via `-stop-service-at-end`.
16+
17+
To run the service on its own (e.g. when iterating against a local checkout of `sdk-test-harness`):
18+
19+
```bash
20+
yarn contract-test-service
21+
```
22+
23+
Then run the harness from your local clone in another terminal.
24+
25+
## Suppressions
26+
27+
Two suppression files cover tests that are not yet supported or are known to differ:
28+
29+
- `testharness-suppressions.txt` -- default
30+
- `testharness-suppressions-fdv2.txt` -- when running the harness from the `feat/fdv2` branch
31+
32+
Override the suppressions file by setting the `SUPPRESSIONS` environment variable:
33+
34+
```bash
35+
SUPPRESSIONS=./contract-tests/testharness-suppressions-fdv2.txt yarn contract-tests
36+
```
37+
38+
## Other environment variables
39+
40+
- `TEST_HARNESS_PARAMS` -- extra params appended to the harness command line (e.g. `-run TestName`).
41+
- `VERSION` -- the major version of `sdk-test-harness` to download. Defaults to `v2`.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "@launchdarkly/node-client-sdk-contract-tests",
3+
"version": "0.0.0",
4+
"main": "dist/src/index.js",
5+
"scripts": {
6+
"start": "node --inspect dist/src/index.js",
7+
"build": "tsc",
8+
"dev": "tsc --watch"
9+
},
10+
"type": "module",
11+
"author": "",
12+
"license": "Apache-2.0",
13+
"private": true,
14+
"dependencies": {
15+
"@launchdarkly/js-contract-test-utils": "workspace:^",
16+
"@launchdarkly/node-client-sdk": "workspace:^",
17+
"body-parser": "^1.19.0",
18+
"express": "^4.17.1"
19+
},
20+
"devDependencies": {
21+
"@types/body-parser": "^1.19.2",
22+
"@types/express": "^4.17.13",
23+
"@types/node": "^18.11.9",
24+
"typescript": "^5.5.3"
25+
}
26+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/sh
2+
# Runs the SDK contract tests locally against a fresh build of @launchdarkly/node-client-sdk.
3+
#
4+
# Mirrors the GitHub Actions workflow at .github/workflows/node-client.yml: builds the SDK
5+
# and its contract-test service, starts the service in the background, downloads the matching
6+
# sdk-test-harness binary, and runs the harness against the service.
7+
#
8+
# Environment variables:
9+
# SUPPRESSIONS Path to the suppressions file to pass via --skip-from. Defaults to
10+
# ./testharness-suppressions.txt (next to this script). Use
11+
# ./testharness-suppressions-fdv2.txt when running the harness from the
12+
# feat/fdv2 branch.
13+
# TEST_HARNESS_PARAMS Extra params appended to the harness command line.
14+
# VERSION sdk-test-harness major version to download. Defaults to v2.
15+
16+
set -e
17+
18+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
19+
SUPPRESSIONS="${SUPPRESSIONS:-$SCRIPT_DIR/testharness-suppressions.txt}"
20+
VERSION="${VERSION:-v2}"
21+
22+
yarn workspaces foreach -pR --topological-dev --from '@launchdarkly/node-client-sdk' run build
23+
yarn workspace @launchdarkly/node-client-sdk-contract-tests build
24+
25+
yarn workspace @launchdarkly/node-client-sdk-contract-tests start &
26+
SERVICE_PID=$!
27+
trap 'kill $SERVICE_PID 2>/dev/null || true' EXIT
28+
29+
curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/main/downloader/run.sh \
30+
| VERSION="$VERSION" \
31+
PARAMS="-url http://localhost:8000 -debug -stop-service-at-end --skip-from=$SUPPRESSIONS $TEST_HARNESS_PARAMS" \
32+
sh
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import bodyParser from 'body-parser';
2+
import express, { Request, Response } from 'express';
3+
import { Server } from 'http';
4+
5+
import { ClientPool } from '@launchdarkly/js-contract-test-utils';
6+
7+
import { Log } from './log.js';
8+
import { badCommandError, newSdkClientEntity, SdkClientEntity } from './sdkClientEntity.js';
9+
10+
const app = express();
11+
let server: Server | null = null;
12+
13+
const port = 8000;
14+
15+
const clients = new ClientPool<SdkClientEntity>();
16+
17+
const mainLog = Log('service');
18+
19+
app.use(bodyParser.json());
20+
21+
app.get('/', (req: Request, res: Response) => {
22+
res.header('Content-Type', 'application/json');
23+
res.json({
24+
capabilities: [
25+
'client-side',
26+
'service-endpoints',
27+
'tags',
28+
'user-type',
29+
'inline-context',
30+
'inline-context-all',
31+
'client-prereq-events',
32+
'client-per-context-summaries',
33+
'evaluation-hooks',
34+
'track-hooks',
35+
'anonymous-redaction',
36+
'strongly-typed',
37+
'event-gzip',
38+
'flag-change-listeners',
39+
'tls:skip-verify-peer',
40+
'tls:custom-ca',
41+
'wrapper',
42+
],
43+
});
44+
});
45+
46+
app.delete('/', (req: Request, res: Response) => {
47+
mainLog.info('Test service has told us to exit');
48+
res.status(204);
49+
res.send();
50+
51+
// Defer the following actions till after the response has been sent
52+
setTimeout(() => {
53+
if (server) {
54+
server.close(() => process.exit());
55+
}
56+
// We force-quit with process.exit because, even after closing the server, there could be some
57+
// scheduled tasks lingering if an SDK instance didn't get cleaned up properly.
58+
}, 1);
59+
});
60+
61+
app.post('/', async (req: Request, res: Response) => {
62+
const options = req.body;
63+
64+
try {
65+
const client = await newSdkClientEntity(options);
66+
const clientId = clients.add(client);
67+
68+
res.status(201);
69+
res.set('Location', `/clients/${clientId}`);
70+
} catch (e) {
71+
res.status(500);
72+
const message = e instanceof Error ? e.message : JSON.stringify(e);
73+
mainLog.error(`Error creating client: ${message}`);
74+
res.write(message);
75+
}
76+
res.send();
77+
});
78+
79+
app.post('/clients/:id', async (req: Request, res: Response) => {
80+
const client = clients.get(req.params.id);
81+
if (!client) {
82+
res.status(404);
83+
} else {
84+
try {
85+
const respValue = await client.doCommand(req.body);
86+
if (respValue) {
87+
res.status(200);
88+
res.write(JSON.stringify(respValue));
89+
} else {
90+
res.status(204);
91+
}
92+
} catch (e) {
93+
const isBadRequest = e === badCommandError;
94+
res.status(isBadRequest ? 400 : 500);
95+
const message = e instanceof Error ? e.message : JSON.stringify(e);
96+
res.write(message);
97+
if (!isBadRequest && e instanceof Error && e.stack) {
98+
// eslint-disable-next-line no-console
99+
console.log(e.stack);
100+
}
101+
}
102+
}
103+
res.send();
104+
});
105+
106+
app.delete('/clients/:id', async (req: Request, res: Response) => {
107+
const client = clients.get(req.params.id);
108+
if (!client) {
109+
res.status(404);
110+
res.send();
111+
} else {
112+
await client.close();
113+
clients.remove(req.params.id);
114+
res.status(204);
115+
res.send();
116+
}
117+
});
118+
119+
server = app.listen(port, () => {
120+
// eslint-disable-next-line no-console
121+
console.log('Listening on port %d', port);
122+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { basicLogger, LDLogger } from '@launchdarkly/node-client-sdk';
2+
3+
export interface Logger {
4+
info: (message: string) => void;
5+
error: (message: string) => void;
6+
}
7+
8+
export function Log(tag: string): Logger {
9+
function doLog(level: string, message: string): void {
10+
// eslint-disable-next-line no-console
11+
console.log(`${new Date().toISOString()} [${tag}] ${level}: ${message}`);
12+
}
13+
return {
14+
info: (message: string) => doLog('info', message),
15+
error: (message: string) => doLog('error', message),
16+
};
17+
}
18+
19+
export function sdkLogger(tag: string): LDLogger {
20+
return basicLogger({
21+
level: 'debug',
22+
destination: (line: string) => {
23+
// eslint-disable-next-line no-console
24+
console.log(`${new Date().toISOString()} [${tag}.sdk] ${line}`);
25+
},
26+
});
27+
}

0 commit comments

Comments
 (0)