Skip to content

Commit 34d45e4

Browse files
committed
test: contrast tests for Oxygen sdk
1 parent 00e9505 commit 34d45e4

File tree

9 files changed

+275
-1
lines changed

9 files changed

+275
-1
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"packages/telemetry/browser-telemetry",
4343
"contract-tests",
4444
"packages/sdk/combined-browser",
45-
"packages/sdk/shopify-oxygen"
45+
"packages/sdk/shopify-oxygen",
46+
"packages/sdk/shopify-oxygen/contract-tests"
4647
],
4748
"private": true,
4849
"scripts": {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.log
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "@launchdarkly/shopify-oxygen-contract-tests",
3+
"version": "0.0.0",
4+
"main": "dist/index.js",
5+
"scripts": {
6+
"start": "node --inspect dist/index.js",
7+
"build": "tsup",
8+
"dev": "tsc --watch"
9+
},
10+
"type": "module",
11+
"license": "Apache-2.0",
12+
"private": true,
13+
"dependencies": {
14+
"@launchdarkly/js-server-sdk-common": "workspace:^",
15+
"@launchdarkly/shopify-oxygen-sdk": "workspace:^",
16+
"express": "^5.0.1"
17+
},
18+
"devDependencies": {
19+
"@types/express": "^5.0.1",
20+
"@types/node": "^18.11.9",
21+
"tsup": "^8.5.0",
22+
"typescript": "^4.9.0"
23+
}
24+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
3+
# If sdk-test-harness is not in the path, then you will need to set
4+
# the SDK_TEST_HARNESS environment variable to the path to the sdk-test-harness binary.
5+
6+
# Default to the local sdk-test-harness binary if not provided
7+
if [ -z "${SDK_TEST_HARNESS}" ]; then
8+
SDK_TEST_HARNESS="sdk-test-harness"
9+
fi
10+
11+
# Uncomment this to start the test service in the background
12+
# yarn start &> test-service.log &
13+
14+
# skipping all tests that require streaming connections.
15+
${SDK_TEST_HARNESS} --url http://localhost:8000 \
16+
--skip "streaming.*" \
17+
--skip "evaluation.*" \
18+
--skip "event.*" \
19+
--skip "service.*" \
20+
--stop-service-at-end
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import express, { Request, Response } from 'express';
2+
import { Server } from 'http';
3+
4+
import { ClientPool } from './utils';
5+
6+
/* eslint-disable no-console */
7+
8+
// export DEBUG=true to enable debugging
9+
// unset DEBUG to disable debugging
10+
const debugging = process.env.DEBUG === 'true';
11+
12+
const app = express();
13+
let server: Server | null = null;
14+
15+
app.use(express.json());
16+
17+
const port = 8000;
18+
19+
const clientPool = new ClientPool();
20+
21+
if (debugging) {
22+
app.use((req: Request, res: Response, next: Function) => {
23+
console.debug('request', req.method, req.url);
24+
if (req.body) {
25+
console.debug('request', JSON.stringify(req.body, null, 2));
26+
}
27+
next();
28+
});
29+
} else {
30+
// NOOP global console.debug
31+
console.debug = () => {};
32+
}
33+
34+
app.get('/', (req: Request, res: Response) => {
35+
res.header('Content-Type', 'application/json');
36+
res.json({
37+
capabilities: ['server-side-polling', 'server-side'],
38+
});
39+
});
40+
41+
app.delete('/', (req: Request, res: Response) => {
42+
console.log('Test service has told us to exit');
43+
res.status(204);
44+
res.send();
45+
46+
if (server) {
47+
server.close(() => process.exit());
48+
}
49+
});
50+
51+
app.post('/', async (req: Request, res: Response) => {
52+
await clientPool.createClient(req.body, res);
53+
res.send();
54+
});
55+
56+
app.post('/clients/:id', async (req: Request, res: Response) => {
57+
await clientPool.runCommand(req.params.id, req.body, res);
58+
res.send();
59+
});
60+
61+
app.delete('/clients/:id', async (req: Request, res: Response) => {
62+
console.debug('DELETE request received /clients/:id');
63+
console.debug(req.params.id);
64+
await clientPool.deleteClient(req.params.id, res);
65+
res.send();
66+
});
67+
68+
server = app.listen(port, () => {
69+
// eslint-disable-next-line no-console
70+
console.log('Listening on port %d', port);
71+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Response } from 'express';
2+
3+
import { LDClient } from '@launchdarkly/js-server-sdk-common';
4+
import { init } from '@launchdarkly/shopify-oxygen-sdk';
5+
6+
/* eslint-disable no-console */
7+
8+
// NOTE: Currently, this is a very simple client pool that only really handles the
9+
// very limited Oxygen specific use cases... we should be expand this to be more
10+
// general purpose in the future and maybe even come up with some shared ts interface
11+
// to facilitate future contract testing.
12+
13+
/**
14+
* ClientPool is a singleton that manages a pool of LDClient instances. Currently there is
15+
* no separation between a managed client and this pool. Which means all of the client specs
16+
* will be implemented in this class.
17+
*
18+
* @see https://github.com/launchdarkly/sdk-test-harness/blob/v2/docs/service_spec.md
19+
*/
20+
export default class ClientPool {
21+
private _clients: Record<string, LDClient> = {};
22+
private _clientCounter = 0;
23+
24+
constructor() {
25+
this._clients = {};
26+
this._clientCounter = 0;
27+
}
28+
29+
private _makeId(): string {
30+
this._clientCounter += 1;
31+
return `client-${this._clientCounter}`;
32+
}
33+
34+
public async runCommand(id: string, body: any, res: Response): Promise<void> {
35+
const client = this._clients[id];
36+
// TODO: handle the 'itCanFailCase'
37+
if (client) {
38+
try {
39+
const { command, ...rest } = body;
40+
switch (command) {
41+
case 'evaluate': {
42+
const { flagKey, context, defaultValue, detail } = rest.evaluate;
43+
const evaluation = detail
44+
? await client.variationDetail(flagKey, context, defaultValue)
45+
: await client.variation(flagKey, context, defaultValue);
46+
res.status(200);
47+
res.json({ value: evaluation });
48+
break;
49+
}
50+
default: {
51+
res.status(400);
52+
res.json({ error: `Unknown command: ${command}` });
53+
break;
54+
}
55+
}
56+
} catch (err) {
57+
console.error(`Error running command: ${err}`);
58+
res.status(500);
59+
}
60+
} else {
61+
res.status(404);
62+
}
63+
}
64+
65+
public async deleteClient(id: string, res: Response): Promise<void> {
66+
const client = this._clients[id];
67+
if (client) {
68+
client.close();
69+
delete this._clients[id];
70+
res.status(204);
71+
} else {
72+
res.status(404);
73+
}
74+
}
75+
76+
public async createClient(options: any, res: Response): Promise<void> {
77+
try {
78+
const id = this._makeId();
79+
const {
80+
configuration: { credential = 'unknown-sdk-key', polling },
81+
} = options;
82+
83+
if (!polling) {
84+
// We do not support non-polling clients yet
85+
res.status(400);
86+
return;
87+
}
88+
const client = await init(credential, {
89+
...(polling && {
90+
baseUri: polling.baseUri,
91+
}),
92+
});
93+
94+
await client.waitForInitialization({ timeout: 10 });
95+
this._clients[id] = client;
96+
res.status(201);
97+
res.set('Location', `/clients/${id}`);
98+
if (!client.initialized()) {
99+
res.status(500);
100+
client.close();
101+
}
102+
this._clients[id] = client;
103+
console.debug(`Creating client with configuration: ${JSON.stringify(options.configuration)}`);
104+
} catch (err) {
105+
console.error(`Error creating client: ${err}`);
106+
res.status(500);
107+
}
108+
}
109+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as ClientPool } from './clientPool';
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"compilerOptions": {
3+
"allowSyntheticDefaultImports": true,
4+
"declaration": true,
5+
"declarationMap": true,
6+
"lib": ["es6"],
7+
"module": "ES6",
8+
"moduleResolution": "node",
9+
"noImplicitOverride": true,
10+
"outDir": "dist",
11+
"resolveJsonModule": true,
12+
"rootDir": ".",
13+
"skipLibCheck": true,
14+
"sourceMap": true,
15+
"strict": true,
16+
"stripInternal": true,
17+
"target": "ES2017",
18+
},
19+
"include": ["src/**/*"],
20+
"exclude": ["dist", "node_modules"]
21+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// It is a dev dependency and the linter doesn't understand.
2+
// @ts-ignore - tsup is a dev dependency installed at runtime
3+
// eslint-disable-next-line import/no-extraneous-dependencies
4+
import { defineConfig } from 'tsup';
5+
6+
export default defineConfig({
7+
entry: {
8+
index: 'src/index.ts',
9+
},
10+
minify: true,
11+
format: ['esm', 'cjs'],
12+
splitting: false,
13+
sourcemap: false,
14+
clean: true,
15+
dts: true,
16+
metafile: true,
17+
esbuildOptions(opts) {
18+
// This would normally be `^_(?!meta|_)`, but go doesn't support negative look-ahead assertions,
19+
// so we need to craft something that works without it.
20+
// So start of line followed by a character that isn't followed by m or underscore, but we
21+
// want other things that do start with m, so we need to progressively handle more characters
22+
// of meta with exclusions.
23+
// eslint-disable-next-line no-param-reassign
24+
opts.mangleProps = /^_([^m|_]|m[^e]|me[^t]|met[^a])/;
25+
},
26+
});

0 commit comments

Comments
 (0)