Skip to content

Commit 2d5b7f7

Browse files
committed
Add generate unit test for generated http transport
1 parent 0ef9f2c commit 2d5b7f7

6 files changed

Lines changed: 249 additions & 76 deletions

File tree

scripts/generator-adapter/generators/app/index.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ export default class extends Generator.default {
152152
)
153153

154154
// Create endpoint and transport files
155-
Object.values(this.props.endpoints).forEach(({ inputEndpointName, inputTransports, endpointAliases }) => {
155+
Object.values(this.props.endpoints).forEach(({ inputEndpointName, inputTransports, endpointAliases, normalizedEndpointNameCap }) => {
156156
if (inputTransports.length > 1) {
157157
// Router endpoints
158158
this.fs.copyTpl(
@@ -171,7 +171,7 @@ export default class extends Generator.default {
171171
this.fs.copyTpl(
172172
this.templatePath(`src/transport/${transport.type}.ts.ejs`),
173173
this.destinationPath(`${this.args[0]}/${this.props.adapterName}/src/transport/${inputEndpointName}-${transport.type}.ts`),
174-
{ inputEndpointName, includeComments: this.props.includeComments },
174+
{ inputEndpointName, normalizedEndpointNameCap, includeComments: this.props.includeComments }
175175
)
176176
})
177177
} else {
@@ -191,7 +191,7 @@ export default class extends Generator.default {
191191
this.fs.copyTpl(
192192
this.templatePath(`src/transport/${inputTransports[0].type}.ts.ejs`),
193193
this.destinationPath(`${this.args[0]}/${this.props.adapterName}/src/transport/${inputEndpointName}.ts`),
194-
{ inputEndpointName, includeComments: this.props.includeComments },
194+
{ inputEndpointName, normalizedEndpointNameCap, includeComments: this.props.includeComments },
195195
)
196196
}
197197
})
@@ -219,10 +219,20 @@ export default class extends Generator.default {
219219
// Create adapter.test.ts if there is at least one endpoint with httpTransport
220220
if (httpEndpoints.length) {
221221
this.fs.copyTpl(
222-
this.templatePath(`test/adapter.test.ts.ejs`),
222+
this.templatePath(`test/integration/adapter.test.ts.ejs`),
223223
this.destinationPath(`${this.args[0]}/${this.props.adapterName}/test/integration/adapter.test.ts`),
224224
{ endpoints: httpEndpoints, transportName: 'rest', setBgExecuteMsEnv: false },
225225
)
226+
227+
// Create http unit test files
228+
for (const { inputEndpointName, normalizedEndpointNameCap, inputTransports } of httpEndpoints) {
229+
const transportFileBaseName = inputTransports.length > 1 ? `${inputEndpointName}-http` : inputEndpointName
230+
this.fs.copyTpl(
231+
this.templatePath(`test/unit/http.test.ts.ejs`),
232+
this.destinationPath(`${this.args[0]}/${this.props.adapterName}/test/unit/${transportFileBaseName}.test.ts`),
233+
{ inputEndpointName, normalizedEndpointNameCap, transportFileBaseName },
234+
)
235+
}
226236
}
227237

228238
// Create adapter.test.ts or adapter-ws.test.ts if there is at least one endpoint with wsTransport
@@ -232,7 +242,7 @@ export default class extends Generator.default {
232242
fileName = 'adapter-ws.test.ts'
233243
}
234244
this.fs.copyTpl(
235-
this.templatePath(`test/adapter-ws.test.ts.ejs`),
245+
this.templatePath(`test/integration/adapter-ws.test.ts.ejs`),
236246
this.destinationPath(`${this.args[0]}/${this.props.adapterName}/test/integration/${fileName}`),
237247
{ endpoints: wsEndpoints },
238248
)
@@ -248,7 +258,7 @@ export default class extends Generator.default {
248258
fileName = 'adapter-custom-fg.test.ts'
249259
}
250260
this.fs.copyTpl(
251-
this.templatePath(`test/adapter.test.ts.ejs`),
261+
this.templatePath(`test/integration/adapter.test.ts.ejs`),
252262
this.destinationPath(`${this.args[0]}/${this.props.adapterName}/test/integration/${fileName}`),
253263
{ endpoints: customFgEndpoints, transportName: 'customfg', setBgExecuteMsEnv: false },
254264
)
@@ -259,15 +269,15 @@ export default class extends Generator.default {
259269
fileName = 'adapter-custom-bg.test.ts'
260270
}
261271
this.fs.copyTpl(
262-
this.templatePath(`test/adapter.test.ts.ejs`),
272+
this.templatePath(`test/integration/adapter.test.ts.ejs`),
263273
this.destinationPath(`${this.args[0]}/${this.props.adapterName}/test/integration/${fileName}`),
264274
{ endpoints: customBgEndpoints, transportName: 'custombg', setBgExecuteMsEnv: true },
265275
)
266276
}
267277

268278
// Copy test fixtures
269279
this.fs.copyTpl(
270-
this.templatePath(`test/fixtures.ts.ejs`),
280+
this.templatePath(`test/integration/fixtures.ts.ejs`),
271281
this.destinationPath(`${this.args[0]}/${this.props.adapterName}/test/integration/fixtures.ts`),
272282
{
273283
includeWsFixtures: wsEndpoints.length > 0,

scripts/generator-adapter/generators/app/templates/src/transport/http.ts.ejs

Lines changed: 74 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -23,75 +23,81 @@ export type HttpTransportTypes = BaseEndpointTypes & {
2323
// HttpTransport is used to fetch and process data from a Provider using HTTP(S) protocol. It usually needs two methods
2424
// `prepareRequests` and `parseResponse`
2525
<% } -%>
26-
export const httpTransport = new HttpTransport<HttpTransportTypes>({
26+
export class <%= normalizedEndpointNameCap %>HttpTransport extends HttpTransport<HttpTransportTypes> {
27+
constructor() {
28+
super({
2729
<% if (includeComments) { -%>
28-
// `prepareRequests` method receives request payloads sent to associated endpoint alongside adapter config(environment variables)
29-
// and should return 'request information' to the Data Provider. Use this method to construct one or many requests, and the framework
30-
// will send them to Data Provider
31-
<% } -%>
32-
prepareRequests: (params, config) => {
33-
return params.map((param) => {
34-
return {
35-
<% if (includeComments) { -%>
36-
// `params` are parameters associated to this single request and will also be available in the 'parseResponse' method.
37-
<% } -%>
38-
params: [param],
39-
<% if (includeComments) { -%>
40-
// `request` contains any valid axios request configuration
41-
<% } -%>
42-
request: {
43-
baseURL: config.API_ENDPOINT,
44-
url: '/cryptocurrency/price',
45-
headers: {
46-
'X_API_KEY': config.API_KEY,
47-
},
48-
params: {
49-
symbol: param.base.toUpperCase(),
50-
convert: param.quote.toUpperCase(),
51-
},
52-
},
53-
}
54-
})
55-
},
56-
<% if (includeComments) { -%>
57-
// `parseResponse` takes the 'params' specified in the `prepareRequests` and the 'response' from Data Provider and should return
58-
// an array of response objects to be stored in cache. Use this method to construct a list of response objects for every parameter in 'params'
59-
// and the framework will save them in cache and return to user
60-
<% } -%>
61-
parseResponse: (params, response) => {
62-
<% if (includeComments) { -%>
63-
// For each 'param' a new response object is created and returned as an array
64-
<% } -%>
65-
return params.map((param) => {
66-
<% if (includeComments) { -%>
67-
// In case error was received, it's a good practice to return meaningful information to user
68-
<% } -%>
69-
const baseSymbol = param.base.toUpperCase()
70-
if (!response.data || !response.data[baseSymbol]) {
71-
return {
72-
params: param,
73-
response: {
74-
errorMessage: `The data provider didn't return any value for ${param.base}/${param.quote}`,
75-
statusCode: 502,
76-
},
77-
}
78-
}
30+
// `prepareRequests` method receives request payloads sent to associated endpoint alongside adapter config(environment variables)
31+
// and should return 'request information' to the Data Provider. Use this method to construct one or many requests, and the framework
32+
// will send them to Data Provider
33+
<% } -%>
34+
prepareRequests: (params, config) => {
35+
return params.map((param) => {
36+
return {
37+
<% if (includeComments) { -%>
38+
// `params` are parameters associated to this single request and will also be available in the 'parseResponse' method.
39+
<% } -%>
40+
params: [param],
41+
<% if (includeComments) { -%>
42+
// `request` contains any valid axios request configuration
43+
<% } -%>
44+
request: {
45+
baseURL: config.API_ENDPOINT,
46+
url: '/cryptocurrency/price',
47+
headers: {
48+
'X_API_KEY': config.API_KEY,
49+
},
50+
params: {
51+
symbol: param.base.toUpperCase(),
52+
convert: param.quote.toUpperCase(),
53+
},
54+
},
55+
}
56+
})
57+
},
58+
<% if (includeComments) { -%>
59+
// `parseResponse` takes the 'params' specified in the `prepareRequests` and the 'response' from Data Provider and should return
60+
// an array of response objects to be stored in cache. Use this method to construct a list of response objects for every parameter in 'params'
61+
// and the framework will save them in cache and return to user
62+
<% } -%>
63+
parseResponse: (params, response) => {
64+
<% if (includeComments) { -%>
65+
// For each 'param' a new response object is created and returned as an array
66+
<% } -%>
67+
return params.map((param) => {
68+
<% if (includeComments) { -%>
69+
// In case error was received, it's a good practice to return meaningful information to user
70+
<% } -%>
71+
const baseSymbol = param.base.toUpperCase()
72+
if (!response.data || !response.data[baseSymbol]) {
73+
return {
74+
params: param,
75+
response: {
76+
errorMessage: `The data provider didn't return any value for ${param.base}/${param.quote}`,
77+
statusCode: 502,
78+
},
79+
}
80+
}
7981

80-
const result = response.data[baseSymbol].price
81-
<% if (includeComments) { -%>
82-
// Response objects, whether successful or errors, contain two properties, 'params' and 'response'. 'response' is what will be
83-
// stored in the cache and returned as adapter response and 'params' determines the identifier so that the next request with same 'params'
84-
// will immediately return the response from the cache
85-
<% } -%>
86-
return {
87-
params: param,
88-
response: {
89-
result,
90-
data: {
91-
result
82+
const result = response.data[baseSymbol].price
83+
<% if (includeComments) { -%>
84+
// Response objects, whether successful or errors, contain two properties, 'params' and 'response'. 'response' is what will be
85+
// stored in the cache and returned as adapter response and 'params' determines the identifier so that the next request with same 'params'
86+
// will immediately return the response from the cache
87+
<% } -%>
88+
return {
89+
params: param,
90+
response: {
91+
result,
92+
data: {
93+
result,
94+
},
95+
},
9296
}
93-
},
94-
}
97+
})
98+
},
9599
})
96-
},
97-
})
100+
}
101+
}
102+
103+
export const httpTransport = new <%= normalizedEndpointNameCap %>HttpTransport()

scripts/generator-adapter/generators/app/templates/test/adapter-ws.test.ts.ejs renamed to scripts/generator-adapter/generators/app/templates/test/integration/adapter-ws.test.ts.ejs

File renamed without changes.

scripts/generator-adapter/generators/app/templates/test/adapter.test.ts.ejs renamed to scripts/generator-adapter/generators/app/templates/test/integration/adapter.test.ts.ejs

File renamed without changes.

scripts/generator-adapter/generators/app/templates/test/fixtures.ts.ejs renamed to scripts/generator-adapter/generators/app/templates/test/integration/fixtures.ts.ejs

File renamed without changes.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { EndpointContext } from '@chainlink/external-adapter-framework/adapter'
2+
import { calculateHttpRequestKey } from '@chainlink/external-adapter-framework/cache'
3+
import { metrics } from '@chainlink/external-adapter-framework/metrics'
4+
import { TransportDependencies } from '@chainlink/external-adapter-framework/transports'
5+
import { LoggerFactoryProvider } from '@chainlink/external-adapter-framework/util'
6+
import { makeStub } from '@chainlink/external-adapter-framework/util/testing-utils'
7+
import { inputParameters } from '../../src/endpoint/<%= inputEndpointName %>'
8+
import { <%= normalizedEndpointNameCap %>HttpTransport, HttpTransportTypes } from '../../src/transport/<%= transportFileBaseName %>'
9+
10+
const log = jest.fn()
11+
const debugLog = jest.fn()
12+
const logger = {
13+
fatal: log,
14+
error: log,
15+
warn: log,
16+
info: log,
17+
debug: debugLog,
18+
trace: debugLog,
19+
msgPrefix: 'mock-logger',
20+
}
21+
22+
const loggerFactory = { child: () => logger }
23+
24+
LoggerFactoryProvider.set(loggerFactory)
25+
metrics.initialize()
26+
27+
describe('<%= normalizedEndpointNameCap %>HttpTransport', () => {
28+
const transportName = 'default_single_transport'
29+
const endpointName = '<%= inputEndpointName %>'
30+
const apiKey = 'test-api-key'
31+
const apiUrl = 'http://api.example.com'
32+
33+
const adapterSettings = makeStub('adapterSettings', {
34+
API_ENDPOINT: apiUrl,
35+
API_KEY: apiKey,
36+
CACHE_MAX_AGE: 90_000,
37+
MAX_COMMON_KEY_SIZE: 300,
38+
WARMUP_SUBSCRIPTION_TTL: 10_000,
39+
} as unknown as HttpTransportTypes['Settings'])
40+
41+
const subscriptionSet = makeStub('subscriptionSet', {
42+
getAll: jest.fn(),
43+
})
44+
45+
const subscriptionSetFactory = makeStub('subscriptionSetFactory', {
46+
buildSet() {
47+
return subscriptionSet
48+
},
49+
})
50+
51+
const requester = makeStub('requester', {
52+
request: jest.fn(),
53+
})
54+
55+
const responseCache = {
56+
write: jest.fn(),
57+
}
58+
const dependencies = makeStub('dependencies', {
59+
requester,
60+
responseCache,
61+
subscriptionSetFactory,
62+
} as unknown as TransportDependencies<HttpTransportTypes>)
63+
64+
let transport: <%= normalizedEndpointNameCap %>HttpTransport
65+
66+
const requestKeyForParams = (params: typeof inputParameters.validated) => {
67+
const requestKey = calculateHttpRequestKey<HttpTransportTypes>({
68+
context: {
69+
adapterSettings,
70+
inputParameters,
71+
endpointName,
72+
},
73+
data: [params],
74+
transportName,
75+
})
76+
return requestKey
77+
}
78+
79+
beforeEach(async () => {
80+
jest.restoreAllMocks()
81+
jest.useFakeTimers()
82+
83+
transport = new <%= normalizedEndpointNameCap %>HttpTransport()
84+
85+
await transport.initialize(dependencies, adapterSettings, endpointName, transportName)
86+
})
87+
88+
afterEach(() => {
89+
expect(log).not.toHaveBeenCalled()
90+
})
91+
92+
it('should make the request', async () => {
93+
const btcPrice = 75_000
94+
const params = makeStub('params', {
95+
base: 'BTC',
96+
quote: 'USD',
97+
})
98+
subscriptionSet.getAll.mockReturnValue([params])
99+
100+
const context = makeStub('context', {
101+
adapterSettings,
102+
endpointName,
103+
} as EndpointContext<HttpTransportTypes>)
104+
105+
const response = makeStub('response', {
106+
response: {
107+
data: {
108+
BTC: {
109+
price: btcPrice,
110+
},
111+
cost: undefined,
112+
},
113+
},
114+
timestamps: {},
115+
})
116+
117+
requester.request.mockResolvedValue(response)
118+
119+
await transport.backgroundExecute(context)
120+
121+
const expectedRequestConfig = {
122+
headers: {
123+
X_API_KEY: adapterSettings.API_KEY,
124+
},
125+
baseURL: adapterSettings.API_ENDPOINT,
126+
url: '/cryptocurrency/price',
127+
params: {
128+
symbol: params.base,
129+
convert: params.quote,
130+
},
131+
}
132+
const expectedRequestKey = requestKeyForParams(params)
133+
134+
const expectedResponse = {
135+
result: btcPrice,
136+
data: {
137+
result: btcPrice,
138+
},
139+
timestamps: {},
140+
}
141+
142+
expect(requester.request).toHaveBeenCalledWith(
143+
expectedRequestKey,
144+
expectedRequestConfig,
145+
undefined,
146+
)
147+
expect(requester.request).toHaveBeenCalledTimes(1)
148+
149+
expect(responseCache.write).toHaveBeenCalledWith(transportName, [
150+
{
151+
params,
152+
response: expectedResponse,
153+
},
154+
])
155+
expect(responseCache.write).toHaveBeenCalledTimes(1)
156+
})
157+
})

0 commit comments

Comments
 (0)