Skip to content

Commit fbe0086

Browse files
authored
implement contract tests (#13)
1 parent e9ca870 commit fbe0086

File tree

8 files changed

+251
-2
lines changed

8 files changed

+251
-2
lines changed

.circleci/config.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ node-template: &node-template
2525
- store_artifacts:
2626
path: reports/junit
2727

28+
- run: make build-contract-tests
29+
- run:
30+
command: make start-contract-test-service
31+
background: true
32+
- run: make run-contract-tests
33+
2834
jobs:
2935
oldest-long-term-support-release:
3036
<<: *node-template

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/node_modules/
1+
node_modules
22
npm-debug.log
33
.DS_Store
44
yarn.lock

Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
2+
TEMP_TEST_OUTPUT=/tmp/sse-contract-test-service.log
3+
4+
build-contract-tests:
5+
@cd contract-tests && npm install
6+
7+
start-contract-test-service:
8+
@cd contract-tests && node index.js
9+
10+
start-contract-test-service-bg:
11+
@echo "Test service output will be captured in $(TEMP_TEST_OUTPUT)"
12+
@make start-contract-test-service >$(TEMP_TEST_OUTPUT) 2>&1 &
13+
14+
run-contract-tests:
15+
@curl -s https://raw.githubusercontent.com/launchdarkly/sse-contract-tests/v0.0.3/downloader/run.sh \
16+
| VERSION=v0 PARAMS="-url http://localhost:8000 -debug -stop-service-at-end" sh
17+
18+
contract-tests: build-contract-tests start-contract-test-service-bg run-contract-tests
19+
20+
.PHONY: build-contract-tests start-contract-test-service run-contract-tests contract-tests

contract-tests/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# SSE client contract test service
2+
3+
This directory contains an implementation of the cross-platform SSE testing protocol defined by https://github.com/launchdarkly/sse-contract-tests. See that project's `README` for details of this protocol, and the kinds of SSE client capabilities that are relevant to the contract tests. This code should not need to be updated unless the SSE client has added or removed such capabilities.
4+
5+
To run these tests locally, run `make contract-tests` from the project root directory. This downloads the correct version of the test harness tool automatically.

contract-tests/index.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const http = require('http');
2+
const url = require('url');
3+
4+
const express = require('express');
5+
const bodyParser = require('body-parser');
6+
7+
const { StreamEntity } = require('./streamEntity');
8+
9+
const app = express();
10+
11+
const port = 8000;
12+
13+
let streamCounter = 0;
14+
const streams = {};
15+
16+
app.use(bodyParser.json());
17+
18+
app.get('/', (req, res) => {
19+
res.header("Content-Type", "application/json");
20+
res.json({
21+
'capabilities': [
22+
'event-type-listeners',
23+
'headers',
24+
'post',
25+
'read-timeout',
26+
'report',
27+
]
28+
})
29+
});
30+
31+
app.delete('/', (req, res) => {
32+
console.log('Test service has told us to exit');
33+
res.status(204);
34+
res.send();
35+
process.exit();
36+
});
37+
38+
app.post('/', (req, res) => {
39+
const options = req.body;
40+
const tag = options.tag;
41+
42+
if (!options.streamUrl || !options.callbackUrl) {
43+
log(tag, `Received request with incomplete parameters: ${JSON.stringify(options)}`);
44+
res.status(400);
45+
res.send();
46+
return;
47+
}
48+
49+
streamCounter += 1;
50+
const streamId = streamCounter.toString()
51+
const streamResourceUrl = `/streams/${streamId}`;
52+
53+
const stream = StreamEntity(options);
54+
streams[streamId] = stream;
55+
56+
res.status(201);
57+
res.set('Location', streamResourceUrl);
58+
res.send();
59+
});
60+
61+
app.post('/streams/:id', function(req, res) {
62+
const stream = streams[req.params.id];
63+
if (!stream) {
64+
res.status(404);
65+
} else if (!stream.doCommand(req.body)) {
66+
res.status(400);
67+
} else {
68+
res.status(204);
69+
}
70+
res.send();
71+
});
72+
73+
app.delete('/streams/:id', function(req, res) {
74+
const stream = streams[req.params.id];
75+
if (!stream) {
76+
res.status(404);
77+
res.send();
78+
return;
79+
}
80+
stream.close();
81+
delete streams[req.params.id];
82+
res.status(204);
83+
res.send();
84+
});
85+
86+
var server = app.listen(port, function () {
87+
console.log('Listening on port %d', port);
88+
});

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": "sse-contract-tests",
3+
"version": "0.0.0",
4+
"main": "index.js",
5+
"scripts": {
6+
"start": "node index.js"
7+
},
8+
"author": "",
9+
"license": "ISC",
10+
"dependencies": {
11+
"body-parser": "^1.19.0",
12+
"express": "^4.17.1",
13+
"launchdarkly-eventsource": "file:.."
14+
}
15+
}

contract-tests/streamEntity.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
const http = require('http');
2+
const url = require('url');
3+
4+
const { EventSource } = require('launchdarkly-eventsource');
5+
6+
function StreamEntity(options) {
7+
const s = {};
8+
const listeningForType = {};
9+
const tag = options.tag;
10+
let closed = false;
11+
let callbackCounter = 0;
12+
13+
function log(s) {
14+
console.log(`[${tag}] INFO: ${s}`);
15+
}
16+
17+
function logError(tag, s) {
18+
console.log(`[${tag}] ERROR: ${s}`);
19+
}
20+
21+
function sendMessage(message) {
22+
if (closed) {
23+
return;
24+
}
25+
callbackCounter++;
26+
const callbackUrl = options.callbackUrl + '/' + callbackCounter;
27+
const reqParams = {
28+
...url.parse(callbackUrl),
29+
method: 'POST',
30+
headers: { 'Content-Type': 'application/json '}
31+
};
32+
const req = http.request(reqParams, res => {
33+
if (!closed && res.statusCode >= 300) {
34+
logError(`Callback to ${callbackUrl} returned HTTP error ${res.statusCode}`);
35+
}
36+
});
37+
req.on('error', e => {
38+
if (!closed) {
39+
logError(`Callback to ${callbackUrl} failed: ${e}`);
40+
}
41+
});
42+
req.write(JSON.stringify(message));
43+
req.end();
44+
}
45+
46+
log(`Starting stream from ${options.streamUrl}`);
47+
48+
const eventSourceParams = {};
49+
if (options.headers) {
50+
eventSourceParams.headers = options.headers;
51+
}
52+
if (options.method) {
53+
eventSourceParams.method = options.method;
54+
eventSourceParams.body = options.body;
55+
}
56+
if (options.readTimeoutMs) {
57+
eventSourceParams.readTimeoutMillis = options.readTimeoutMs;
58+
}
59+
if (options.initialDelayMs) {
60+
eventSourceParams.initialRetryDelayMillis = options.initialDelayMs;
61+
}
62+
63+
s.onMessage = event => {
64+
log(`Received message from stream (${event.type})`);
65+
sendMessage({
66+
kind: 'event',
67+
event: {
68+
type: event.type,
69+
data: event.data,
70+
id: event.lastEventId
71+
}
72+
});
73+
};
74+
75+
s.close = () => {
76+
s.closed = true;
77+
s.sse.close();
78+
log('Test ended');
79+
};
80+
81+
s.doCommand = params => {
82+
switch (params.command) {
83+
case 'listen':
84+
if (!listeningForType[params.type]) {
85+
listeningForType[params.type] = true;
86+
s.sse.addEventListener(params.type, s.onMessage);
87+
}
88+
return true;
89+
90+
default:
91+
return false;
92+
}
93+
}
94+
95+
s.sse = new EventSource(options.streamUrl, eventSourceParams);
96+
97+
s.sse.onopen = () => {
98+
log('Opened stream');
99+
};
100+
s.sse.onclosed = () => {
101+
log('Closed stream');
102+
};
103+
s.sse.onmessage = s.onMessage;
104+
s.sse.onerror = error => {
105+
log(`Received error from stream: ${error}`);
106+
sendMessage({
107+
kind: 'error',
108+
error: error.toString()
109+
});
110+
};
111+
112+
return s;
113+
}
114+
115+
module.exports.StreamEntity = StreamEntity;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"webpack": "^3.5.6"
4343
},
4444
"scripts": {
45-
"test": "mocha --reporter spec --exit && standard",
45+
"test": "mocha --reporter spec --exit && standard 'src/**/*.js' 'test/**/*.js'",
4646
"polyfill": "webpack lib/eventsource-polyfill.js example/eventsource-polyfill.js",
4747
"coverage": "nyc --reporter=html --reporter=text _mocha --reporter spec"
4848
},

0 commit comments

Comments
 (0)