Skip to content

Commit 6c3e127

Browse files
committed
feat(context): support for creating fake context for testing
1 parent 42d7c43 commit 6c3e127

File tree

5 files changed

+193
-53
lines changed

5 files changed

+193
-53
lines changed

adonis-typings/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,30 @@
1313
/// <reference types="@poppinss/logger/build/adonis-typings" />
1414

1515
declare module '@ioc:Adonis/Src/HttpContext' {
16-
import { HttpContextContract as BaseContextContract } from '@poppinss/http-server/contracts'
16+
import { HttpContextContract as BaseContextContract, ServerConfig } from '@poppinss/http-server/contracts'
1717
import { ResponseContract } from '@ioc:Adonis/Src/Response'
1818
import { RequestContract } from '@ioc:Adonis/Src/Request'
1919
import { LoggerContract } from '@ioc:Adonis/Src/Logger'
20+
import { IncomingMessage, ServerResponse } from 'http'
2021

21-
interface HttpContextContract extends BaseContextContract {
22+
export interface HttpContextContract extends BaseContextContract {
2223
response: ResponseContract,
2324
request: RequestContract,
2425
logger: LoggerContract,
2526
}
27+
28+
export interface HttpContextConstructorContract {
29+
create (
30+
routePattern: string,
31+
routeParams: any,
32+
req?: IncomingMessage,
33+
res?: ServerResponse,
34+
serverConfig?: ServerConfig,
35+
): HttpContextContract
36+
}
37+
38+
const HttpContext: HttpContextConstructorContract
39+
export default HttpContext
2640
}
2741

2842
/**

src/HttpContext/index.ts

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313

1414
/// <reference path="../contracts.ts" />
1515

16-
import { RouteNode, HttpContextContract } from '@poppinss/http-server/contracts'
17-
import { RequestContract } from '@poppinss/request'
18-
import { ResponseContract } from '@poppinss/response'
19-
import { LoggerContract } from '@poppinss/logger'
16+
import { Socket } from 'net'
17+
import { IncomingMessage, ServerResponse } from 'http'
18+
import { RequestContract, Request } from '@poppinss/request'
19+
import { LoggerContract, getLogger } from '@poppinss/logger'
20+
import { ResponseContract, Response } from '@poppinss/response'
21+
import { RouteNode, HttpContextContract, ServerConfig } from '@poppinss/http-server/contracts'
22+
import { makeUrl, getServerConfig } from '../helpers'
2023

2124
/**
2225
* Http context is passed to all route handlers, middleware,
@@ -31,6 +34,75 @@ export class HttpContext implements HttpContextContract {
3134
public request: RequestContract,
3235
public response: ResponseContract,
3336
public logger: LoggerContract,
37+
) {}
38+
39+
/**
40+
* Creates a new fake context instance for a given route.
41+
*/
42+
public static create (
43+
routePattern: string,
44+
routeParams: any,
45+
req?: IncomingMessage,
46+
res?: ServerResponse,
47+
serverConfig?: ServerConfig,
3448
) {
49+
req = req || new IncomingMessage(new Socket())
50+
res = res || new ServerResponse(req)
51+
52+
/**
53+
* Composing server config
54+
*/
55+
serverConfig = getServerConfig(serverConfig || {})
56+
57+
/**
58+
* Creating the url from the router pattern and params. Only
59+
* when actual URL isn't defined.
60+
*/
61+
req.url = req.url || makeUrl(routePattern, { params: routeParams })
62+
63+
/**
64+
* Creating new request instance
65+
*/
66+
const request = new Request(req, res, {
67+
allowMethodSpoofing: serverConfig.allowMethodSpoofing,
68+
subdomainOffset: serverConfig.subdomainOffset,
69+
trustProxy: serverConfig.trustProxy,
70+
})
71+
72+
/**
73+
* Creating new response instance
74+
*/
75+
const response = new Response(req, res, {
76+
etag: serverConfig.etag,
77+
cookie: serverConfig.cookie,
78+
jsonpCallbackName: serverConfig.jsonpCallbackName,
79+
})
80+
81+
/**
82+
* Creating new ctx instance
83+
*/
84+
const ctx = new HttpContext(request, response, getLogger({
85+
name: 'adonis',
86+
enabled: true,
87+
level: 'trace',
88+
messageKey: 'msg',
89+
}))
90+
91+
/**
92+
* Attaching route to the ctx
93+
*/
94+
ctx.route = {
95+
pattern: routePattern,
96+
middleware: [],
97+
handler: async () => 'handled',
98+
meta: {},
99+
}
100+
101+
/**
102+
* Attaching params to the ctx
103+
*/
104+
ctx.params = routeParams
105+
106+
return ctx
35107
}
36108
}

src/Router/index.ts

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@
1313

1414
/// <reference path="../contracts.ts" />
1515

16-
import { stringify } from 'querystring'
17-
import { Exception } from '@poppinss/utils'
1816
import {
1917
RouteMatchers,
2018
RouteNode,
@@ -23,13 +21,14 @@ import {
2321
RouteLookupNode,
2422
RouteHandlerNode,
2523
} from '@poppinss/http-server/contracts'
24+
import { Exception } from '@poppinss/utils'
2625

2726
import { Route } from './Route'
2827
import { RouteResource } from './Resource'
2928
import { RouteGroup } from './Group'
3029
import { BriskRoute } from './BriskRoute'
3130
import { Store } from './Store'
32-
import { toRoutesJSON, exceptionCodes } from '../helpers'
31+
import { toRoutesJSON, exceptionCodes, makeUrl } from '../helpers'
3332

3433
/**
3534
* Router class exposes unified API to create new routes, group them or
@@ -359,47 +358,7 @@ export class Router<Context> implements RouterContract<Context> {
359358
return null
360359
}
361360

362-
let url = matchingRoute.pattern
363-
364-
if (url.indexOf(':') > -1) {
365-
/**
366-
* Split pattern when route has dynamic segments
367-
*/
368-
const tokens = url.split('/')
369-
370-
/**
371-
* Lookup over the route tokens and replace them the params values
372-
*/
373-
url = tokens.map((token) => {
374-
if (!token.startsWith(':')) {
375-
return token
376-
}
377-
378-
const isOptional = token.endsWith('?')
379-
const paramName = token.replace(/^:/, '').replace(/\?$/, '')
380-
const param = options.params[paramName]
381-
382-
/**
383-
* A required param is always required to make the complete URL
384-
*/
385-
if (!param && !isOptional) {
386-
throw new Exception(
387-
`\`${paramName}\` param is required to make URL for \`${matchingRoute.pattern}\` route`,
388-
500,
389-
exceptionCodes.E_MISSING_ROUTE_PARAM_VALUE,
390-
)
391-
}
392-
393-
return param
394-
}).join('/')
395-
}
396-
397-
/**
398-
* Stringify query string and append to the URL (if exists)
399-
*/
400-
const qs = stringify(options.qs)
401-
url = qs ? `${url}?${qs}` : url
402-
361+
const url = makeUrl(matchingRoute.pattern, options)
403362
return matchingRoute.domain !== 'root' ? `//${matchingRoute.domain}${url}` : url
404363
}
405364

src/helpers.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313

1414
/// <reference path="./contracts.ts" />
1515

16-
import { RouteDefination } from '@poppinss/http-server/contracts'
16+
import { stringify } from 'querystring'
17+
import * as proxyAddr from 'proxy-addr'
18+
import { Exception } from '@poppinss/utils'
1719

1820
import { Route } from './Router/Route'
19-
import { RouteResource } from './Router/Resource'
20-
import { BriskRoute } from './Router/BriskRoute'
2121
import { RouteGroup } from './Router/Group'
22+
import { BriskRoute } from './Router/BriskRoute'
23+
import { RouteResource } from './Router/Resource'
24+
import { RouteDefination, ServerConfig } from '@poppinss/http-server/contracts'
2225

2326
/**
2427
* Makes input string consistent by having only the starting
@@ -65,6 +68,68 @@ export function toRoutesJSON<Context extends any> (
6568
}, [])
6669
}
6770

71+
/**
72+
* Makes url for a route pattern and params and querystring.
73+
*/
74+
export function makeUrl (pattern: string, options: { params?: any, qs?: any }): string {
75+
let url = pattern
76+
77+
if (url.indexOf(':') > -1) {
78+
/**
79+
* Split pattern when route has dynamic segments
80+
*/
81+
const tokens = url.split('/')
82+
83+
/**
84+
* Lookup over the route tokens and replace them the params values
85+
*/
86+
url = tokens.map((token) => {
87+
if (!token.startsWith(':')) {
88+
return token
89+
}
90+
91+
const isOptional = token.endsWith('?')
92+
const paramName = token.replace(/^:/, '').replace(/\?$/, '')
93+
const param = options.params[paramName]
94+
95+
/**
96+
* A required param is always required to make the complete URL
97+
*/
98+
if (!param && !isOptional) {
99+
throw new Exception(
100+
`\`${paramName}\` param is required to make URL for \`${pattern}\` route`,
101+
500,
102+
exceptionCodes.E_MISSING_ROUTE_PARAM_VALUE,
103+
)
104+
}
105+
106+
return param
107+
}).join('/')
108+
}
109+
110+
/**
111+
* Stringify query string and append to the URL (if exists)
112+
*/
113+
const qs = stringify(options.qs)
114+
return qs ? `${url}?${qs}` : url
115+
}
116+
117+
/**
118+
* Returns server config by merging the user options with the default
119+
* options.
120+
*/
121+
export function getServerConfig (serverConfig: Partial<ServerConfig>): ServerConfig {
122+
return Object.assign({
123+
secret: Math.random().toFixed(36).substring(2, 38),
124+
subdomainOffset: 2,
125+
allowMethodSpoofing: true,
126+
etag: false,
127+
cookie: {},
128+
jsonpCallbackName: 'callback',
129+
trustProxy: proxyAddr.compile('loopback'),
130+
}, serverConfig)
131+
}
132+
68133
/**
69134
* Module wide exception codes
70135
*/

test/http-context.spec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* @poppinss/http-server
3+
*
4+
* (c) Harminder Virk <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import * as test from 'japa'
11+
import { HttpContext } from '../src/HttpContext'
12+
13+
test.group('Http Context', () => {
14+
test('create fake Http context instance', async (assert) => {
15+
const ctx = HttpContext.create('/', {})
16+
17+
assert.instanceOf(ctx, HttpContext)
18+
assert.equal(ctx.route!.pattern, '/')
19+
assert.deepEqual(ctx.route!.middleware, [])
20+
})
21+
22+
test('compute request url from params', async (assert) => {
23+
const ctx = HttpContext.create('/:id', { id: '1' })
24+
25+
assert.instanceOf(ctx, HttpContext)
26+
assert.equal(ctx.route!.pattern, '/:id')
27+
assert.equal(ctx.request.url(), '/1')
28+
assert.deepEqual(ctx.params, { id: '1' })
29+
})
30+
})

0 commit comments

Comments
 (0)