Skip to content

Commit 2c09c3b

Browse files
authored
chore: Migrate contract tests to typescript. (#820)
Convert the contract tests to TypeScript. This is just a straight conversion without any structural improvements. Add the contract tests to the workspace. This allows everything to be linked locally by default.
1 parent 5cb00cb commit 2c09c3b

16 files changed

+395
-195
lines changed

Diff for: .github/workflows/server-node.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ jobs:
3131
with:
3232
workspace_name: '@launchdarkly/node-server-sdk'
3333
workspace_path: packages/sdk/server-node
34+
- name: Install contract test service dependencies
35+
run: yarn workspace node-server-sdk-contract-tests install --no-immutable
36+
- name: Build the test service
37+
run: yarn contract-test-service-build
3438
- name: Launch the test service in the background
35-
run: yarn run contract-test-service 2>&1 &
39+
run: yarn contract-test-service 2>&1 &
3640
- uses: launchdarkly/gh-actions/actions/[email protected]
3741
with:
3842
test_service_port: 8000

Diff for: contract-tests/BigSegmentTestStore.js

-31
This file was deleted.

Diff for: contract-tests/TestHook.js

-51
This file was deleted.

Diff for: contract-tests/log.js

-20
This file was deleted.

Diff for: contract-tests/package.json

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
{
22
"name": "node-server-sdk-contract-tests",
33
"version": "0.0.0",
4-
"main": "index.js",
4+
"main": "dist/src/index.js",
55
"scripts": {
6-
"start": "node --inspect index.js"
6+
"start": "node --inspect dist/src/index.js",
7+
"build": "tsc",
8+
"dev": "tsc --watch"
79
},
810
"type": "module",
911
"author": "",
1012
"license": "Apache-2.0",
13+
"private": true,
1114
"dependencies": {
15+
"@launchdarkly/node-server-sdk": "9.8.0",
1216
"body-parser": "^1.19.0",
1317
"express": "^4.17.1",
14-
"node-server-sdk": "file:../packages/sdk/server-node",
15-
"got": "13.0.0"
18+
"got": "14.4.7"
19+
},
20+
"devDependencies": {
21+
"@types/body-parser": "^1.19.2",
22+
"@types/express": "^4.17.13",
23+
"@types/node": "^18.11.9",
24+
"typescript": "^4.9.0"
1625
}
1726
}

Diff for: contract-tests/src/BigSegmentTestStore.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import got from 'got';
2+
3+
interface BigSegmentMetadata {
4+
lastUpToDate?: number;
5+
}
6+
7+
interface BigSegmentMembership {
8+
values?: Record<string, boolean>;
9+
}
10+
11+
export default class BigSegmentTestStore {
12+
private _callbackUri: string;
13+
14+
/**
15+
* Create a big segment test store suitable for use with the contract tests.
16+
* @param callbackUri Uri on the test service to direct big segments calls to.
17+
*/
18+
constructor(callbackUri: string) {
19+
this._callbackUri = callbackUri;
20+
}
21+
22+
async getMetadata(): Promise<BigSegmentMetadata> {
23+
const data = await got.get(`${this._callbackUri}/getMetadata`, { retry: { limit: 0 } }).json();
24+
return data as BigSegmentMetadata;
25+
}
26+
27+
async getUserMembership(contextHash: string): Promise<Record<string, boolean> | undefined> {
28+
const data = await got
29+
.post(`${this._callbackUri}/getMembership`, {
30+
retry: { limit: 0 },
31+
json: {
32+
contextHash,
33+
},
34+
})
35+
.json();
36+
return (data as BigSegmentMembership)?.values;
37+
}
38+
39+
close(): void {}
40+
}

Diff for: contract-tests/src/TestHook.ts

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import got from 'got';
2+
3+
import { integrations, LDEvaluationDetail } from '@launchdarkly/node-server-sdk';
4+
5+
export interface HookData {
6+
beforeEvaluation?: Record<string, unknown>;
7+
afterEvaluation?: Record<string, unknown>;
8+
}
9+
10+
export interface HookErrors {
11+
beforeEvaluation?: string;
12+
afterEvaluation?: string;
13+
}
14+
15+
export default class TestHook implements integrations.Hook {
16+
private _name: string;
17+
private _endpoint: string;
18+
private _data?: HookData;
19+
private _errors?: HookErrors;
20+
21+
constructor(name: string, endpoint: string, data?: HookData, errors?: HookErrors) {
22+
this._name = name;
23+
this._endpoint = endpoint;
24+
this._data = data;
25+
this._errors = errors;
26+
}
27+
28+
private async _safePost(body: unknown): Promise<void> {
29+
try {
30+
await got.post(this._endpoint, { json: body });
31+
} catch {
32+
// The test could move on before the post, so we are ignoring
33+
// failed posts.
34+
}
35+
}
36+
37+
getMetadata(): integrations.HookMetadata {
38+
return {
39+
name: this._name,
40+
};
41+
}
42+
43+
beforeEvaluation(
44+
hookContext: integrations.EvaluationSeriesContext,
45+
data: integrations.EvaluationSeriesData,
46+
): integrations.EvaluationSeriesData {
47+
if (this._errors?.beforeEvaluation) {
48+
throw new Error(this._errors.beforeEvaluation);
49+
}
50+
this._safePost({
51+
evaluationSeriesContext: hookContext,
52+
evaluationSeriesData: data,
53+
stage: 'beforeEvaluation',
54+
});
55+
return { ...data, ...(this._data?.beforeEvaluation || {}) };
56+
}
57+
58+
afterEvaluation(
59+
hookContext: integrations.EvaluationSeriesContext,
60+
data: integrations.EvaluationSeriesData,
61+
detail: LDEvaluationDetail,
62+
): integrations.EvaluationSeriesData {
63+
if (this._errors?.afterEvaluation) {
64+
throw new Error(this._errors.afterEvaluation);
65+
}
66+
this._safePost({
67+
evaluationSeriesContext: hookContext,
68+
evaluationSeriesData: data,
69+
stage: 'afterEvaluation',
70+
evaluationDetail: detail,
71+
});
72+
73+
return { ...data, ...(this._data?.afterEvaluation || {}) };
74+
}
75+
}

Diff for: contract-tests/index.js renamed to contract-tests/src/index.ts

+20-14
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
import bodyParser from 'body-parser';
2-
import express from 'express';
2+
import express, { Request, Response } from 'express';
3+
import { Server } from 'http';
34

45
import { Log } from './log.js';
5-
import { badCommandError, newSdkClientEntity } from './sdkClientEntity.js';
6+
import { badCommandError, newSdkClientEntity, SdkClientEntity } from './sdkClientEntity.js';
67

78
const app = express();
8-
let server = null;
9+
let server: Server | null = null;
910

1011
const port = 8000;
1112

1213
let clientCounter = 0;
13-
const clients = {};
14+
const clients: Record<string, SdkClientEntity> = {};
1415

1516
const mainLog = Log('service');
1617

1718
app.use(bodyParser.json());
1819

19-
app.get('/', (req, res) => {
20+
app.get('/', (req: Request, res: Response) => {
2021
res.header('Content-Type', 'application/json');
2122
res.json({
2223
capabilities: [
@@ -45,21 +46,23 @@ app.get('/', (req, res) => {
4546
});
4647
});
4748

48-
app.delete('/', (req, res) => {
49+
app.delete('/', (req: Request, res: Response) => {
4950
mainLog.info('Test service has told us to exit');
5051
res.status(204);
5152
res.send();
5253

5354
// Defer the following actions till after the response has been sent
5455
setTimeout(() => {
55-
server.close(() => process.exit());
56+
if (server) {
57+
server.close(() => process.exit());
58+
}
5659
// We force-quit with process.exit because, even after closing the server, there could be some
5760
// scheduled tasks lingering if an SDK instance didn't get cleaned up properly, and we don't want
5861
// that to prevent us from quitting.
5962
}, 1);
6063
});
6164

62-
app.post('/', async (req, res) => {
65+
app.post('/', async (req: Request, res: Response) => {
6366
const options = req.body;
6467

6568
clientCounter += 1;
@@ -74,14 +77,14 @@ app.post('/', async (req, res) => {
7477
res.set('Location', resourceUrl);
7578
} catch (e) {
7679
res.status(500);
77-
const message = e.message || JSON.stringify(e);
78-
mainLog.error('Error creating client: ' + message);
80+
const message = e instanceof Error ? e.message : JSON.stringify(e);
81+
mainLog.error(`Error creating client: ${message}`);
7982
res.write(message);
8083
}
8184
res.send();
8285
});
8386

84-
app.post('/clients/:id', async (req, res) => {
87+
app.post('/clients/:id', async (req: Request, res: Response) => {
8588
const client = clients[req.params.id];
8689
if (!client) {
8790
res.status(404);
@@ -97,16 +100,18 @@ app.post('/clients/:id', async (req, res) => {
97100
} catch (e) {
98101
const isBadRequest = e === badCommandError;
99102
res.status(isBadRequest ? 400 : 500);
100-
res.write(e.message || JSON.stringify(e));
101-
if (!isBadRequest && e.stack) {
103+
const message = e instanceof Error ? e.message : JSON.stringify(e);
104+
res.write(message);
105+
if (!isBadRequest && e instanceof Error && e.stack) {
106+
// eslint-disable-next-line no-console
102107
console.log(e.stack);
103108
}
104109
}
105110
}
106111
res.send();
107112
});
108113

109-
app.delete('/clients/:id', async (req, res) => {
114+
app.delete('/clients/:id', async (req: Request, res: Response) => {
110115
const client = clients[req.params.id];
111116
if (!client) {
112117
res.status(404);
@@ -120,5 +125,6 @@ app.delete('/clients/:id', async (req, res) => {
120125
});
121126

122127
server = app.listen(port, () => {
128+
// eslint-disable-next-line no-console
123129
console.log('Listening on port %d', port);
124130
});

Diff for: contract-tests/src/log.ts

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import ld from '@launchdarkly/node-server-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): ld.LDLogger {
20+
return ld.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)