-
Notifications
You must be signed in to change notification settings - Fork 821
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[instrumentation-fetch] refactor fetch() tests for clarity, type safety and realism #5268
base: next
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -176,7 +176,7 @@ function testForCorrectEvents( | |
|
||
describe('fetch', () => { | ||
let contextManager: ZoneContextManager; | ||
let lastResponse: any | undefined; | ||
let lastResponse: Response | undefined; | ||
let requestBody: any | undefined; | ||
let webTracerWithZone: api.Tracer; | ||
let webTracerProviderWithZone: WebTracerProvider; | ||
|
@@ -210,12 +210,21 @@ describe('fetch', () => { | |
sinon.stub(core.otperformance, 'now').callsFake(() => fakeNow); | ||
|
||
function fakeFetch(input: RequestInfo | Request, init: RequestInit = {}) { | ||
// Once upon a time, there was a bug (#2411), causing a `Request` object | ||
// to be incorrectly passed to `fetch()` as the second argument | ||
assert.ok(!(init instanceof Request)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a replacement for the |
||
|
||
return new Promise((resolve, reject) => { | ||
const response: any = { | ||
args: {}, | ||
url: fileUrl, | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variable is really the "default response body", but it also serves a double-duty as the To be clear, no tests actually cares that the mock "server response body" has the status in it. |
||
response.headers = Object.assign({}, init.headers); | ||
const responseInit: ResponseInit = {}; | ||
|
||
// This is the default response body expected by the few tests that cares | ||
let responseBody = JSON.stringify({ | ||
isServerResponse: true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
request: { | ||
url: fileUrl, | ||
headers: { ...init.headers }, | ||
}, | ||
}); | ||
|
||
// get the request body | ||
if (typeof input === 'string') { | ||
|
@@ -235,28 +244,22 @@ describe('fetch', () => { | |
} else { | ||
input.text().then(r => (requestBody = r)); | ||
} | ||
|
||
if (init instanceof Request) { | ||
// Passing request as 2nd argument causes missing body bug (#2411) | ||
response.status = 400; | ||
response.statusText = 'Bad Request (Request object as 2nd argument)'; | ||
reject(new window.Response(JSON.stringify(response), response)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This branch was replaced entirely. Note that:
In any case, this branch is not needed anymore. |
||
} else if (init.method === 'DELETE') { | ||
response.status = 405; | ||
response.statusText = 'OK'; | ||
resolve(new window.Response('foo', response)); | ||
if (init.method === 'DELETE') { | ||
responseInit.status = 405; | ||
responseInit.statusText = 'OK'; | ||
responseBody = 'foo'; | ||
} else if ( | ||
(input instanceof Request && input.url === url) || | ||
input === url | ||
) { | ||
response.status = 200; | ||
response.statusText = 'OK'; | ||
resolve(new window.Response(JSON.stringify(response), response)); | ||
responseInit.status = 200; | ||
responseInit.statusText = 'OK'; | ||
} else { | ||
response.status = 404; | ||
response.statusText = 'Bad request'; | ||
reject(new window.Response(JSON.stringify(response), response)); | ||
responseInit.status = 404; | ||
responseInit.statusText = 'Not found'; | ||
} | ||
|
||
resolve(new window.Response(responseBody, responseInit)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We now always resolve, as the real If we are rejecting here because it's an internal assertion that indicating some test is set up wrong, I'd rather see this reject with an regular Error, or just throw synchronously at the top. |
||
}); | ||
} | ||
|
||
|
@@ -321,26 +324,16 @@ describe('fetch', () => { | |
api.trace.setSpan(api.context.active(), rootSpan), | ||
async () => { | ||
fakeNow = 0; | ||
try { | ||
const responsePromise = apiCall(); | ||
fakeNow = 300; | ||
const response = await responsePromise; | ||
|
||
// if the url is not ignored, body.read should be called by now | ||
// awaiting for the span to end | ||
if (readSpy.callCount > 0) await spanEnded; | ||
|
||
// this is a bit tricky as the only way to get all request headers from | ||
// fetch is to use json() | ||
lastResponse = await response.json(); | ||
const headers: { [key: string]: string } = {}; | ||
Object.keys(lastResponse.headers).forEach(key => { | ||
headers[key.toLowerCase()] = lastResponse.headers[key]; | ||
}); | ||
lastResponse.headers = headers; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the convoluted normalization that didn't turn out to be needed (other than removing this part, most of the diff is just indentation change from removing the now-unecessary try/catch. As far as I can tell, (other than the previously unfortunate variable naming) all this would do is to normalize the request headers into lower case. However, nothing broke when I stopped doing this, so evidently the two tests that cares don't actually need this. |
||
} catch (e) { | ||
lastResponse = undefined; | ||
} | ||
lastResponse = undefined; | ||
|
||
const responsePromise = apiCall(); | ||
fakeNow = 300; | ||
lastResponse = await responsePromise; | ||
|
||
// if the url is not ignored, body.read should be called by now | ||
// awaiting for the span to end | ||
if (readSpy.callCount > 0) await spanEnded; | ||
|
||
await sinon.clock.runAllAsync(); | ||
} | ||
); | ||
|
@@ -531,20 +524,24 @@ describe('fetch', () => { | |
]); | ||
}); | ||
|
||
it('should set trace headers', () => { | ||
it('should set trace headers', async () => { | ||
const span: api.Span = exportSpy.args[1][0][0]; | ||
assert.ok(lastResponse instanceof Response); | ||
|
||
const { request } = await lastResponse.json(); | ||
|
||
assert.strictEqual( | ||
lastResponse.headers[X_B3_TRACE_ID], | ||
request.headers[X_B3_TRACE_ID], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The previously badly named variable made this very confusing, but this is actually asserting that we made the request with the correct headers. Specifically that when the URL is not ignored, the instrumentation code will add these additional headers (which then gets put into the mock server response). |
||
span.spanContext().traceId, | ||
`trace header '${X_B3_TRACE_ID}' not set` | ||
); | ||
assert.strictEqual( | ||
lastResponse.headers[X_B3_SPAN_ID], | ||
request.headers[X_B3_SPAN_ID], | ||
span.spanContext().spanId, | ||
`trace header '${X_B3_SPAN_ID}' not set` | ||
); | ||
assert.strictEqual( | ||
lastResponse.headers[X_B3_SAMPLED], | ||
request.headers[X_B3_SAMPLED], | ||
String(span.spanContext().traceFlags), | ||
`trace header '${X_B3_SAMPLED}' not set` | ||
); | ||
|
@@ -593,18 +590,6 @@ describe('fetch', () => { | |
assert.ok(init.headers.get('foo') === 'bar'); | ||
}); | ||
|
||
it('should pass request object as first parameter to the original function (#2411)', () => { | ||
const r = new Request(url); | ||
return window.fetch(r).then( | ||
() => { | ||
assert.ok(true); | ||
}, | ||
(response: Response) => { | ||
assert.fail(response.statusText); | ||
} | ||
); | ||
}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test was added for #2411. It is basically testing that nothing breaks when passing a |
||
|
||
it('should NOT clear the resources', () => { | ||
assert.strictEqual( | ||
clearResourceTimingsSpy.args.length, | ||
|
@@ -627,19 +612,23 @@ describe('fetch', () => { | |
sinon.restore(); | ||
}); | ||
|
||
it('should NOT set trace headers', () => { | ||
it('should NOT set trace headers', async () => { | ||
assert.ok(lastResponse instanceof Response); | ||
|
||
const { request } = await lastResponse.json(); | ||
|
||
assert.strictEqual( | ||
lastResponse.headers[X_B3_TRACE_ID], | ||
request.headers[X_B3_TRACE_ID], | ||
undefined, | ||
`trace header '${X_B3_TRACE_ID}' should not be set` | ||
); | ||
assert.strictEqual( | ||
lastResponse.headers[X_B3_SPAN_ID], | ||
request.headers[X_B3_SPAN_ID], | ||
undefined, | ||
`trace header '${X_B3_SPAN_ID}' should not be set` | ||
); | ||
assert.strictEqual( | ||
lastResponse.headers[X_B3_SAMPLED], | ||
request.headers[X_B3_SAMPLED], | ||
undefined, | ||
`trace header '${X_B3_SAMPLED}' should not be set` | ||
); | ||
|
@@ -959,7 +948,7 @@ describe('fetch', () => { | |
|
||
await prepare(url, applyCustomAttributes); | ||
const rsp = await response.json(); | ||
assert.deepStrictEqual(rsp.args, {}); | ||
assert.strictEqual(rsp.isServerResponse, true); | ||
}); | ||
}); | ||
|
||
|
@@ -971,22 +960,21 @@ describe('fetch', () => { | |
ignoreUrls: [propagateTraceHeaderCorsUrls], | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
clearData(); | ||
}); | ||
|
||
it('should NOT create any span', () => { | ||
assert.strictEqual(exportSpy.args.length, 0, "span shouldn't b exported"); | ||
}); | ||
it('should pass request object as the first parameter to the original function (#2411)', () => { | ||
const r = new Request(url); | ||
return window.fetch(r).then( | ||
() => { | ||
assert.ok(true); | ||
}, | ||
(response: Response) => { | ||
assert.fail(response.statusText); | ||
} | ||
); | ||
|
||
it('should accept Request objects as argument (#2411)', async () => { | ||
const response = await window.fetch(new Request(url)); | ||
assert.ok(response instanceof Response); | ||
|
||
const { isServerResponse } = await response.json(); | ||
assert.strictEqual(isServerResponse, true); | ||
}); | ||
}); | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This changes more than the type! Despite the name, this variable previously wasn't storing the
Response
object of the lastfetch()
request. It stores something that is kind of the parsed response body (~=await response.json()
) but also with some additional bespoke processing/normalization that didn't turn out to be necessary.This changes things so
lastResponse
actually stores theResponse
object from the lastfetch()
request made in the test. Any test that cares about the response body can callawait lastResponse.json()
in the test itself and do what it needs to do there.