Skip to content

Commit 8135a51

Browse files
authored
feat (datafile manager): Node datafile manager (#3)
Summary: Adds `NodeDatafileManager` class, extending `HttpPollingDatafileManager` with Node-specific http requesting logic. Test plan: Unit tests Issues: https://optimizely.atlassian.net/browse/OASIS-4309
1 parent 1c80d37 commit 8135a51

File tree

8 files changed

+477
-23
lines changed

8 files changed

+477
-23
lines changed

packages/datafile-manager/__test__/httpPollingDatafileManager.spec.ts

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,8 @@
1616

1717
import HTTPPollingDatafileManager from '../src/httpPollingDatafileManager'
1818
import { Headers, AbortableRequest, Response } from '../src/http'
19-
import { TimeoutFactory } from '../src/timeoutFactory'
2019
import { DatafileManagerConfig } from '../src/datafileManager';
21-
22-
class TestTimeoutFactory implements TimeoutFactory {
23-
timeoutFns: Array<() => void> = []
24-
25-
cancelFns: Array<() => void> = []
26-
27-
setTimeout(onTimeout: () => void, timeout: number): () => void {
28-
const cancelFn = jest.fn()
29-
this.timeoutFns.push(() => {
30-
onTimeout()
31-
})
32-
this.cancelFns.push(cancelFn)
33-
return cancelFn
34-
}
35-
36-
cleanup() {
37-
this.timeoutFns = []
38-
this.cancelFns = []
39-
}
40-
}
20+
import TestTimeoutFactory from './testTimeoutFactory'
4121

4222
// Test implementation:
4323
// - Does not make any real requests: just resolves with queued responses (tests push onto queuedResponses)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Copyright 2019, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import NodeDatafileManager from '../src/nodeDatafileManager'
18+
import * as nodeRequest from '../src/nodeRequest'
19+
import { Headers, AbortableRequest } from '../src/http'
20+
import TestTimeoutFactory from './testTimeoutFactory'
21+
22+
describe('nodeDatafileManager', () => {
23+
const testTimeoutFactory: TestTimeoutFactory = new TestTimeoutFactory()
24+
25+
let makeGetRequestSpy: jest.SpyInstance<AbortableRequest, [string, Headers]>
26+
beforeEach(() => {
27+
makeGetRequestSpy = jest.spyOn(nodeRequest, 'makeGetRequest')
28+
})
29+
30+
afterEach(() => {
31+
jest.restoreAllMocks()
32+
testTimeoutFactory.cleanup()
33+
})
34+
35+
it('calls nodeEnvironment.makeGetRequest when started', async () => {
36+
makeGetRequestSpy.mockReturnValue({
37+
abort: jest.fn(),
38+
responsePromise: Promise.resolve({
39+
statusCode: 200,
40+
body: '{"foo":"bar"}',
41+
headers: {},
42+
})
43+
})
44+
45+
const manager = new NodeDatafileManager({
46+
sdkKey: '1234',
47+
liveUpdates: false,
48+
})
49+
manager.start()
50+
expect(makeGetRequestSpy).toBeCalledTimes(1)
51+
expect(makeGetRequestSpy.mock.calls[0][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json')
52+
expect(makeGetRequestSpy.mock.calls[0][1]).toEqual({})
53+
54+
await manager.onReady
55+
await manager.stop()
56+
})
57+
58+
it('calls nodeEnvironment.makeGetRequest for live update requests', async () => {
59+
makeGetRequestSpy.mockReturnValue({
60+
abort: jest.fn(),
61+
responsePromise: Promise.resolve({
62+
statusCode: 200,
63+
body: '{"foo":"bar"}',
64+
headers: {
65+
'last-modified': 'Fri, 08 Mar 2019 18:57:17 GMT',
66+
},
67+
})
68+
})
69+
const manager = new NodeDatafileManager({
70+
sdkKey: '1234',
71+
liveUpdates: true,
72+
timeoutFactory: testTimeoutFactory,
73+
})
74+
manager.start()
75+
await manager.onReady
76+
testTimeoutFactory.timeoutFns[0]()
77+
expect(makeGetRequestSpy).toBeCalledTimes(2)
78+
expect(makeGetRequestSpy.mock.calls[1][0]).toBe('https://cdn.optimizely.com/datafiles/1234.json')
79+
expect(makeGetRequestSpy.mock.calls[1][1]).toEqual({
80+
'if-modified-since': 'Fri, 08 Mar 2019 18:57:17 GMT'
81+
})
82+
83+
await manager.stop()
84+
})
85+
})
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* Copyright 2019, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import nock from 'nock'
18+
import { makeGetRequest } from '../src/nodeRequest'
19+
20+
beforeAll(() => {
21+
nock.disableNetConnect()
22+
})
23+
24+
afterAll(() => {
25+
nock.enableNetConnect()
26+
})
27+
28+
describe('nodeEnvironment', () => {
29+
const host = 'https://cdn.optimizely.com'
30+
const path = '/datafiles/123.json'
31+
32+
afterEach(async () => {
33+
nock.cleanAll()
34+
})
35+
36+
describe('makeGetRequest', () => {
37+
it('returns a 200 response back to its superclass', async () => {
38+
const scope = nock(host)
39+
.get(path)
40+
.reply(200, '{"foo":"bar"}')
41+
const req = makeGetRequest(`${host}${path}`, {})
42+
const resp = await req.responsePromise
43+
expect(resp).toEqual({
44+
statusCode: 200,
45+
body: '{"foo":"bar"}',
46+
headers: {},
47+
})
48+
scope.done()
49+
})
50+
51+
it('returns a 404 response back to its superclass', async () => {
52+
const scope = nock(host)
53+
.get(path)
54+
.reply(404, '')
55+
const req = makeGetRequest(`${host}${path}`, {})
56+
const resp = await req.responsePromise
57+
expect(resp).toEqual({
58+
statusCode: 404,
59+
body: '',
60+
headers: {},
61+
})
62+
scope.done()
63+
})
64+
65+
it('includes headers from the headers argument in the request', async () => {
66+
const scope = nock(host)
67+
.matchHeader('if-modified-since', 'Fri, 08 Mar 2019 18:57:18 GMT')
68+
.get(path)
69+
.reply(304, '')
70+
const req = makeGetRequest(`${host}${path}`, {
71+
'if-modified-since': 'Fri, 08 Mar 2019 18:57:18 GMT',
72+
})
73+
const resp = await req.responsePromise
74+
expect(resp).toEqual({
75+
statusCode: 304,
76+
body: '',
77+
headers: {},
78+
})
79+
scope.done()
80+
})
81+
82+
it('includes headers from the response in the eventual response in the return value', async () => {
83+
const scope = nock(host)
84+
.get(path)
85+
.reply(200, { foo: 'bar' }, {
86+
'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT',
87+
})
88+
const req = makeGetRequest(`${host}${path}`, {})
89+
const resp = await req.responsePromise
90+
expect(resp).toEqual({
91+
statusCode: 200,
92+
body: '{"foo":"bar"}',
93+
headers: {
94+
'content-type': 'application/json',
95+
'last-modified': 'Fri, 08 Mar 2019 18:57:18 GMT',
96+
},
97+
})
98+
scope.done()
99+
})
100+
101+
it('handles a URL with a query string', async () => {
102+
const pathWithQuery = '/datafiles/123.json?from_my_app=true'
103+
const scope = nock(host)
104+
.get(pathWithQuery)
105+
.reply(200, { foo: 'bar' })
106+
const req = makeGetRequest(`${host}${pathWithQuery}`, {})
107+
await req.responsePromise
108+
scope.done()
109+
})
110+
111+
it('handles a URL with http protocol (not https)', async () => {
112+
const httpHost = 'http://cdn.optimizely.com'
113+
const scope = nock(httpHost)
114+
.get(path)
115+
.reply(200, '{"foo":"bar"}')
116+
const req = makeGetRequest(`${httpHost}${path}`, {})
117+
const resp = await req.responsePromise
118+
expect(resp).toEqual({
119+
statusCode: 200,
120+
body: '{"foo":"bar"}',
121+
headers: {}
122+
})
123+
scope.done()
124+
})
125+
126+
it('returns a rejected response promise when the URL protocol is unsupported', async () => {
127+
const invalidProtocolUrl = 'ftp://something/datafiles/123.json'
128+
const req = makeGetRequest(invalidProtocolUrl, {})
129+
await expect(req.responsePromise).rejects.toThrow()
130+
})
131+
132+
it('returns a rejected promise when there is a request error', async () => {
133+
const scope = nock(host)
134+
.get(path)
135+
.replyWithError({
136+
message: 'Connection error',
137+
code: 'CONNECTION_ERROR',
138+
})
139+
const req = makeGetRequest(`${host}${path}`, {})
140+
await expect(req.responsePromise).rejects.toThrow()
141+
scope.done()
142+
})
143+
})
144+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { TimeoutFactory } from '../src/timeoutFactory'
2+
3+
export default class TestTimeoutFactory implements TimeoutFactory {
4+
timeoutFns: Array<() => void> = []
5+
6+
cancelFns: Array<() => void> = []
7+
8+
setTimeout(onTimeout: () => void, timeout: number): () => void {
9+
const cancelFn = jest.fn()
10+
this.timeoutFns.push(() => {
11+
onTimeout()
12+
})
13+
this.cancelFns.push(cancelFn)
14+
return cancelFn
15+
}
16+
17+
cleanup() {
18+
this.timeoutFns = []
19+
this.cancelFns = []
20+
}
21+
}

packages/datafile-manager/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
},
3333
"devDependencies": {
3434
"@types/jest": "^24.0.9",
35+
"@types/nock": "^9.3.1",
3536
"jest": "^24.1.0",
37+
"nock": "^10.0.6",
3638
"ts-jest": "^24.0.0",
3739
"typescript": "^3.3.3333"
3840
},
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Copyright 2019, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { makeGetRequest } from './nodeRequest'
18+
import HttpPollingDatafileManager from './httpPollingDatafileManager'
19+
import { Headers, AbortableRequest } from './http';
20+
21+
export default class NodeDatafileManager extends HttpPollingDatafileManager {
22+
protected makeGetRequest(reqUrl: string, headers: Headers): AbortableRequest {
23+
return makeGetRequest(reqUrl, headers)
24+
}
25+
}

0 commit comments

Comments
 (0)