-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
26fb8fc
commit a58d622
Showing
3 changed files
with
494 additions
and
338 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,174 +2,15 @@ import * as sentryCore from '@sentry/core'; | |
import * as sentryHub from '@sentry/hub'; | ||
import { Hub } from '@sentry/hub'; | ||
import { Transaction } from '@sentry/tracing'; | ||
import { Baggage } from '@sentry/types'; | ||
import { Baggage, Event } from '@sentry/types'; | ||
import { SentryError } from '@sentry/utils'; | ||
import * as http from 'http'; | ||
import * as net from 'net'; | ||
|
||
import { Event, Request, User } from '../src'; | ||
import { NodeClient } from '../src/client'; | ||
import { | ||
errorHandler, | ||
ExpressRequest, | ||
extractRequestData, | ||
parseRequest, | ||
requestHandler, | ||
tracingHandler, | ||
} from '../src/handlers'; | ||
import { errorHandler, requestHandler, tracingHandler } from '../src/handlers'; | ||
import * as SDK from '../src/sdk'; | ||
import { getDefaultNodeClientOptions } from './helper/node-client-options'; | ||
|
||
describe('parseRequest', () => { | ||
let mockReq: { [key: string]: any }; | ||
|
||
beforeEach(() => { | ||
mockReq = { | ||
baseUrl: '/routerMountPath', | ||
body: 'foo', | ||
cookies: { test: 'test' }, | ||
headers: { | ||
host: 'mattrobenolt.com', | ||
}, | ||
method: 'POST', | ||
originalUrl: '/routerMountPath/subpath/specificValue?querystringKey=querystringValue', | ||
path: '/subpath/specificValue', | ||
query: { | ||
querystringKey: 'querystringValue', | ||
}, | ||
route: { | ||
path: '/subpath/:parameterName', | ||
stack: [ | ||
{ | ||
name: 'parameterNameRouteHandler', | ||
}, | ||
], | ||
}, | ||
url: '/subpath/specificValue?querystringKey=querystringValue', | ||
user: { | ||
custom_property: 'foo', | ||
email: '[email protected]', | ||
id: 123, | ||
username: 'tobias', | ||
}, | ||
}; | ||
}); | ||
|
||
describe('parseRequest.user properties', () => { | ||
const DEFAULT_USER_KEYS = ['id', 'username', 'email']; | ||
const CUSTOM_USER_KEYS = ['custom_property']; | ||
|
||
test('parseRequest.user only contains the default properties from the user', () => { | ||
const parsedRequest: Event = parseRequest({}, mockReq as ExpressRequest); | ||
expect(Object.keys(parsedRequest.user as User)).toEqual(DEFAULT_USER_KEYS); | ||
}); | ||
|
||
test('parseRequest.user only contains the custom properties specified in the options.user array', () => { | ||
const parsedRequest: Event = parseRequest({}, mockReq as ExpressRequest, { | ||
user: CUSTOM_USER_KEYS, | ||
}); | ||
expect(Object.keys(parsedRequest.user as User)).toEqual(CUSTOM_USER_KEYS); | ||
}); | ||
|
||
test('parseRequest.user doesnt blow up when someone passes non-object value', () => { | ||
const parsedRequest: Event = parseRequest( | ||
{}, | ||
{ | ||
...mockReq, | ||
// @ts-ignore user is not assignable to object | ||
user: 'wat', | ||
}, | ||
); | ||
expect(Object.keys(parsedRequest.user as User)).toEqual([]); | ||
}); | ||
}); | ||
|
||
describe('parseRequest.ip property', () => { | ||
test('can be extracted from req.ip', () => { | ||
const parsedRequest: Event = parseRequest( | ||
{}, | ||
{ | ||
...mockReq, | ||
ip: '123', | ||
} as ExpressRequest, | ||
{ | ||
ip: true, | ||
}, | ||
); | ||
expect(parsedRequest.user!.ip_address).toEqual('123'); | ||
}); | ||
|
||
test('can extract from req.connection.remoteAddress', () => { | ||
const parsedRequest: Event = parseRequest( | ||
{}, | ||
{ | ||
...mockReq, | ||
connection: { | ||
remoteAddress: '321', | ||
} as net.Socket, | ||
} as ExpressRequest, | ||
{ | ||
ip: true, | ||
}, | ||
); | ||
expect(parsedRequest.user!.ip_address).toEqual('321'); | ||
}); | ||
}); | ||
|
||
describe('parseRequest.request properties', () => { | ||
test('parseRequest.request only contains the default set of properties from the request', () => { | ||
const DEFAULT_REQUEST_PROPERTIES = ['cookies', 'data', 'headers', 'method', 'query_string', 'url']; | ||
const parsedRequest: Event = parseRequest({}, mockReq as ExpressRequest); | ||
expect(Object.keys(parsedRequest.request as Request)).toEqual(DEFAULT_REQUEST_PROPERTIES); | ||
}); | ||
|
||
test('parseRequest.request only contains the specified properties in the options.request array', () => { | ||
const INCLUDED_PROPERTIES = ['data', 'headers', 'query_string', 'url']; | ||
const parsedRequest: Event = parseRequest({}, mockReq as ExpressRequest, { | ||
request: INCLUDED_PROPERTIES, | ||
}); | ||
expect(Object.keys(parsedRequest.request as Request)).toEqual(INCLUDED_PROPERTIES); | ||
}); | ||
|
||
test('parseRequest.request skips `body` property for GET and HEAD requests', () => { | ||
expect(parseRequest({}, mockReq as ExpressRequest, {}).request).toHaveProperty('data'); | ||
expect(parseRequest({}, { ...mockReq, method: 'GET' } as ExpressRequest, {}).request).not.toHaveProperty('data'); | ||
expect(parseRequest({}, { ...mockReq, method: 'HEAD' } as ExpressRequest, {}).request).not.toHaveProperty('data'); | ||
}); | ||
}); | ||
|
||
describe('parseRequest.transaction property', () => { | ||
test('extracts method and full route path by default`', () => { | ||
const parsedRequest: Event = parseRequest({}, mockReq as ExpressRequest); | ||
expect(parsedRequest.transaction).toEqual('POST /routerMountPath/subpath/:parameterName'); | ||
}); | ||
|
||
test('extracts method and full path by default when mountpoint is `/`', () => { | ||
mockReq.originalUrl = mockReq.originalUrl.replace('/routerMountpath', ''); | ||
mockReq.baseUrl = ''; | ||
const parsedRequest: Event = parseRequest({}, mockReq as ExpressRequest); | ||
// "sub"path is the full path here, because there's no router mount path | ||
expect(parsedRequest.transaction).toEqual('POST /subpath/:parameterName'); | ||
}); | ||
|
||
test('fallback to method and `originalUrl` if route is missing', () => { | ||
delete mockReq.route; | ||
const parsedRequest: Event = parseRequest({}, mockReq as ExpressRequest); | ||
expect(parsedRequest.transaction).toEqual('POST /routerMountPath/subpath/specificValue'); | ||
}); | ||
|
||
test('can extract path only instead if configured', () => { | ||
const parsedRequest: Event = parseRequest({}, mockReq as ExpressRequest, { transaction: 'path' }); | ||
expect(parsedRequest.transaction).toEqual('/routerMountPath/subpath/:parameterName'); | ||
}); | ||
|
||
test('can extract handler name instead if configured', () => { | ||
const parsedRequest: Event = parseRequest({}, mockReq as ExpressRequest, { transaction: 'handler' }); | ||
expect(parsedRequest.transaction).toEqual('parameterNameRouteHandler'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('requestHandler', () => { | ||
const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; | ||
const method = 'wagging'; | ||
|
@@ -270,7 +111,7 @@ describe('requestHandler', () => { | |
}); | ||
}); | ||
|
||
it('patches `res.end` when `flushTimeout` is specified', () => { | ||
it('patches `res.end` when `flushTimeout` is specified', done => { | ||
const flush = jest.spyOn(SDK, 'flush').mockResolvedValue(true); | ||
|
||
const sentryRequestMiddleware = requestHandler({ flushTimeout: 1337 }); | ||
|
@@ -280,10 +121,11 @@ describe('requestHandler', () => { | |
setImmediate(() => { | ||
expect(flush).toHaveBeenCalledWith(1337); | ||
expect(res.finished).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
|
||
it('prevents errors thrown during `flush` from breaking the response', async () => { | ||
it('prevents errors thrown during `flush` from breaking the response', done => { | ||
jest.spyOn(SDK, 'flush').mockRejectedValue(new SentryError('HTTP Error (429)')); | ||
|
||
const sentryRequestMiddleware = requestHandler({ flushTimeout: 1337 }); | ||
|
@@ -292,6 +134,7 @@ describe('requestHandler', () => { | |
|
||
setImmediate(() => { | ||
expect(res.finished).toBe(true); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
|
@@ -530,181 +373,6 @@ describe('tracingHandler', () => { | |
}); | ||
}); | ||
|
||
describe('extractRequestData()', () => { | ||
describe('default behaviour', () => { | ||
test('node', () => { | ||
expect( | ||
extractRequestData({ | ||
headers: { host: 'example.com' }, | ||
method: 'GET', | ||
secure: true, | ||
originalUrl: '/', | ||
}), | ||
).toEqual({ | ||
cookies: {}, | ||
headers: { | ||
host: 'example.com', | ||
}, | ||
method: 'GET', | ||
query_string: null, | ||
url: 'https://example.com/', | ||
}); | ||
}); | ||
|
||
test('degrades gracefully without request data', () => { | ||
expect(extractRequestData({})).toEqual({ | ||
cookies: {}, | ||
headers: {}, | ||
method: undefined, | ||
query_string: null, | ||
url: 'http://<no host>', | ||
}); | ||
}); | ||
}); | ||
|
||
describe('cookies', () => { | ||
it('uses `req.cookies` if available', () => { | ||
expect( | ||
extractRequestData( | ||
{ | ||
cookies: { foo: 'bar' }, | ||
}, | ||
['cookies'], | ||
), | ||
).toEqual({ | ||
cookies: { foo: 'bar' }, | ||
}); | ||
}); | ||
|
||
it('parses the cookie header', () => { | ||
expect( | ||
extractRequestData( | ||
{ | ||
headers: { | ||
cookie: 'foo=bar;', | ||
}, | ||
}, | ||
['cookies'], | ||
), | ||
).toEqual({ | ||
cookies: { foo: 'bar' }, | ||
}); | ||
}); | ||
|
||
it('falls back if no cookies are defined', () => { | ||
expect(extractRequestData({}, ['cookies'])).toEqual({ | ||
cookies: {}, | ||
}); | ||
}); | ||
}); | ||
|
||
describe('data', () => { | ||
it('includes data from `req.body` if available', () => { | ||
expect( | ||
extractRequestData( | ||
{ | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | ||
body: 'foo=bar', | ||
}, | ||
['data'], | ||
), | ||
).toEqual({ | ||
data: 'foo=bar', | ||
}); | ||
}); | ||
|
||
it('encodes JSON body contents back to a string', () => { | ||
expect( | ||
extractRequestData( | ||
{ | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: { foo: 'bar' }, | ||
}, | ||
['data'], | ||
), | ||
).toEqual({ | ||
data: '{"foo":"bar"}', | ||
}); | ||
}); | ||
}); | ||
|
||
describe('query_string', () => { | ||
it('parses the query parms from the url', () => { | ||
expect( | ||
extractRequestData( | ||
{ | ||
headers: { host: 'example.com' }, | ||
secure: true, | ||
originalUrl: '/?foo=bar', | ||
}, | ||
['query_string'], | ||
), | ||
).toEqual({ | ||
query_string: 'foo=bar', | ||
}); | ||
}); | ||
|
||
it('gracefully degrades if url cannot be determined', () => { | ||
expect(extractRequestData({}, ['query_string'])).toEqual({ | ||
query_string: null, | ||
}); | ||
}); | ||
}); | ||
|
||
describe('url', () => { | ||
test('express/koa', () => { | ||
expect( | ||
extractRequestData( | ||
{ | ||
host: 'example.com', | ||
protocol: 'https', | ||
url: '/', | ||
}, | ||
['url'], | ||
), | ||
).toEqual({ | ||
url: 'https://example.com/', | ||
}); | ||
}); | ||
|
||
test('node', () => { | ||
expect( | ||
extractRequestData( | ||
{ | ||
headers: { host: 'example.com' }, | ||
secure: true, | ||
originalUrl: '/', | ||
}, | ||
['url'], | ||
), | ||
).toEqual({ | ||
url: 'https://example.com/', | ||
}); | ||
}); | ||
}); | ||
|
||
describe('custom key', () => { | ||
it('includes the custom key if present', () => { | ||
expect( | ||
extractRequestData( | ||
{ | ||
httpVersion: '1.1', | ||
}, | ||
['httpVersion'], | ||
), | ||
).toEqual({ | ||
httpVersion: '1.1', | ||
}); | ||
}); | ||
|
||
it('gracefully degrades if the custom key is missing', () => { | ||
expect(extractRequestData({}, ['httpVersion'])).toEqual({}); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('errorHandler()', () => { | ||
const headers = { ears: 'furry', nose: 'wet', tongue: 'spotted', cookie: 'favorite=zukes' }; | ||
const method = 'wagging'; | ||
|
Oops, something went wrong.