Skip to content

Commit 38f081e

Browse files
authored
feat: Add support for browser contract tests. (#582)
This adds the contract tests, but does not yet automate running them.
1 parent 0848ab7 commit 38f081e

27 files changed

+1001
-14
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
"packages/store/node-server-sdk-dynamodb",
2424
"packages/telemetry/node-server-sdk-otel",
2525
"packages/tooling/jest",
26-
"packages/sdk/browser"
26+
"packages/sdk/browser",
27+
"packages/sdk/browser/contract-tests/entity",
28+
"packages/sdk/browser/contract-tests/adapter"
2729
],
2830
"private": true,
2931
"scripts": {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "browser-contract-test-adapter",
3+
"version": "1.0.0",
4+
"description": "Adapts REST interface to a websocket for use in browsers.",
5+
"main": "dist/index.js",
6+
"scripts": {
7+
"build": "tsc",
8+
"start": "yarn build && node dist/index.js",
9+
"lint": "eslint ./src",
10+
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../../.prettierignore"
11+
},
12+
"author": "",
13+
"license": "UNLICENSED",
14+
"dependencies": {
15+
"body-parser": "^1.20.3",
16+
"cors": "^2.8.5",
17+
"express": "^4.21.0",
18+
"ws": "^8.18.0"
19+
},
20+
"devDependencies": {
21+
"@eslint/js": "^9.10.0",
22+
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
23+
"@types/cors": "^2.8.17",
24+
"@types/express": "^4.17.21",
25+
"@types/ws": "^8.5.12",
26+
"@typescript-eslint/eslint-plugin": "^6.20.0",
27+
"@typescript-eslint/parser": "^6.20.0",
28+
"eslint": "^8.45.0",
29+
"eslint-config-airbnb-base": "^15.0.0",
30+
"eslint-config-airbnb-typescript": "^17.1.0",
31+
"eslint-config-prettier": "^8.8.0",
32+
"eslint-plugin-import": "^2.27.5",
33+
"eslint-plugin-jest": "^27.6.3",
34+
"eslint-plugin-prettier": "^5.0.0",
35+
"globals": "^15.9.0",
36+
"prettier": "^3.0.0",
37+
"typescript": "^5.6.2",
38+
"typescript-eslint": "^8.5.0"
39+
}
40+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/* eslint-disable no-console */
2+
3+
/* eslint-disable @typescript-eslint/no-explicit-any */
4+
import bodyParser from 'body-parser';
5+
import cors from 'cors';
6+
import { randomUUID } from 'crypto';
7+
import express from 'express';
8+
import http from 'node:http';
9+
import util from 'node:util';
10+
import { WebSocketServer } from 'ws';
11+
12+
let server: http.Server | undefined;
13+
14+
async function main() {
15+
const wss = new WebSocketServer({ port: 8001 });
16+
const waiters: Record<string, (data: unknown) => void> = {};
17+
18+
console.log('Running contract test harness adapter.');
19+
wss.on('connection', async (ws) => {
20+
ws.on('error', console.error);
21+
22+
ws.on('message', (stringData: string) => {
23+
const data = JSON.parse(stringData);
24+
if (Object.prototype.hasOwnProperty.call(waiters, data.reqId)) {
25+
waiters[data.reqId](data);
26+
delete waiters[data.reqId];
27+
} else {
28+
console.error('Did not find outstanding request', data.reqId);
29+
}
30+
});
31+
32+
const send = (data: { [key: string]: unknown; reqId: string }): Promise<any> => {
33+
let resolver: (data: unknown) => void;
34+
const waiter = new Promise((resolve) => {
35+
resolver = resolve;
36+
});
37+
// @ts-expect-error The body of the above assignment runs sequentially.
38+
waiters[data.reqId] = resolver;
39+
ws.send(JSON.stringify(data));
40+
return waiter;
41+
};
42+
43+
if (server) {
44+
await util.promisify(server.close).call(server);
45+
server = undefined;
46+
}
47+
48+
const app = express();
49+
50+
const port = 8000;
51+
52+
app.use(
53+
cors({
54+
origin: '*',
55+
allowedHeaders: '*',
56+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
57+
}),
58+
);
59+
app.use(bodyParser.json());
60+
61+
app.get('/', async (_req, res) => {
62+
const commandResult = await send({ command: 'getCapabilities', reqId: randomUUID() });
63+
res.header('Content-Type', 'application/json');
64+
res.json(commandResult);
65+
});
66+
67+
app.delete('/', () => {
68+
process.exit();
69+
});
70+
71+
app.post('/', async (req, res) => {
72+
const commandResult = await send({
73+
command: 'createClient',
74+
body: req.body,
75+
reqId: randomUUID(),
76+
});
77+
if (commandResult.resourceUrl) {
78+
res.set('Location', commandResult.resourceUrl);
79+
}
80+
if (commandResult.status) {
81+
res.status(commandResult.status);
82+
}
83+
res.send();
84+
});
85+
86+
app.post('/clients/:id', async (req, res) => {
87+
const commandResult = await send({
88+
command: 'runCommand',
89+
id: req.params.id,
90+
body: req.body,
91+
reqId: randomUUID(),
92+
});
93+
if (commandResult.status) {
94+
res.status(commandResult.status);
95+
}
96+
if (commandResult.body) {
97+
res.write(JSON.stringify(commandResult.body));
98+
}
99+
res.send();
100+
});
101+
102+
app.delete('/clients/:id', async (req, res) => {
103+
await send({ command: 'deleteClient', id: req.params.id, reqId: randomUUID() });
104+
res.send();
105+
});
106+
107+
server = app.listen(port, () => {
108+
console.log('Listening on port %d', port);
109+
});
110+
});
111+
}
112+
main();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": ["/**/*.ts"],
4+
"exclude": ["node_modules"]
5+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES6",
4+
"module": "commonjs",
5+
"esModuleInterop": true,
6+
"forceConsistentCasingInFileNames": true,
7+
"strict": true,
8+
"moduleResolution": "node",
9+
"outDir": "dist",
10+
"sourceMap": true
11+
},
12+
"lib": ["ES6"],
13+
"exclude": ["**/*.test.ts", "dist", "node_modules"]
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"include": ["src/**/*", "package.json"],
4+
"compilerOptions": {
5+
"composite": true
6+
}
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>SDK Contract Test Service</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="/src/main.ts"></script>
12+
</body>
13+
</html>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "browser-contract-test-service",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"description": "Contract test service implementation for @launchdarkly/js-client-sdk",
7+
"scripts": {
8+
"start": "vite --open=true",
9+
"lint": "eslint ./src",
10+
"prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../../.prettierignore"
11+
},
12+
"dependencies": {
13+
"@launchdarkly/js-client-sdk": "0.0.0"
14+
},
15+
"devDependencies": {
16+
"@trivago/prettier-plugin-sort-imports": "^4.1.1",
17+
"@typescript-eslint/eslint-plugin": "^6.20.0",
18+
"@typescript-eslint/parser": "^6.20.0",
19+
"eslint": "^8.45.0",
20+
"eslint-config-airbnb-base": "^15.0.0",
21+
"eslint-config-airbnb-typescript": "^17.1.0",
22+
"eslint-config-prettier": "^8.8.0",
23+
"eslint-plugin-import": "^2.27.5",
24+
"eslint-plugin-jest": "^27.6.3",
25+
"eslint-plugin-prettier": "^5.0.0",
26+
"prettier": "^3.0.0",
27+
"typescript": "^5.5.3",
28+
"vite": "^5.4.1"
29+
}
30+
}

0 commit comments

Comments
 (0)