Skip to content

Commit 318ba79

Browse files
committed
feat: add server.pipeline method
This pipeline method can be used for writing tests that involves executing middleware chain without making an HTTP request. Useful for packages and not user land code
1 parent 99ff13a commit 318ba79

File tree

3 files changed

+175
-3
lines changed

3 files changed

+175
-3
lines changed

src/server/main.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,17 @@ import type { Logger } from '@adonisjs/logger'
1414
import type { Encryption } from '@adonisjs/encryption'
1515
import type { Server as HttpsServer } from 'node:https'
1616
import type { Application } from '@adonisjs/application'
17-
import { ContainerResolver, moduleImporter } from '@adonisjs/fold'
17+
import { ContainerResolver, moduleCaller, moduleImporter } from '@adonisjs/fold'
1818
import type { ServerResponse, IncomingMessage, Server as HttpServer } from 'node:http'
1919

2020
import type { LazyImport } from '../types/base.js'
2121
import type { MiddlewareAsClass, ParsedGlobalMiddleware } from '../types/middleware.js'
22-
import type { ErrorHandlerAsAClass, ServerConfig, ServerErrorHandler } from '../types/server.js'
22+
import type {
23+
ServerConfig,
24+
ServerErrorHandler,
25+
ErrorHandlerAsAClass,
26+
TestingMiddlewarePipeline,
27+
} from '../types/server.js'
2328

2429
import { Qs } from '../qs.js'
2530
import debug from '../debug.js'
@@ -176,6 +181,35 @@ export class Server {
176181
.finally(writeResponse(ctx))
177182
}
178183

184+
/**
185+
* Creates a pipeline of middleware.
186+
*/
187+
pipeline(middleware: MiddlewareAsClass[]): TestingMiddlewarePipeline {
188+
const middlewareStack = new Middleware<ParsedGlobalMiddleware>()
189+
middleware.forEach((one) => {
190+
middlewareStack.add(moduleCaller(one, 'handle').toHandleMethod())
191+
})
192+
193+
middlewareStack.freeze()
194+
const stackRunner = middlewareStack.runner()
195+
196+
return {
197+
finalHandler(handler) {
198+
stackRunner.finalHandler(handler)
199+
return this
200+
},
201+
errorHandler(handler) {
202+
stackRunner.errorHandler(handler)
203+
return this
204+
},
205+
run(ctx) {
206+
return stackRunner.run((handler, next) => {
207+
return handler.handle(ctx.containerResolver, ctx, next)
208+
})
209+
},
210+
}
211+
}
212+
179213
/**
180214
* Define an array of middleware to use on all the incoming HTTP request.
181215
* Calling this method multiple times pushes to the existing list

src/types/server.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
* file that was distributed with this source code.
88
*/
99

10+
import type { ContainerResolver } from '@adonisjs/fold'
11+
import type { ErrorHandler, FinalHandler } from '@poppinss/middleware/types'
12+
1013
import type { Constructor } from './base.js'
1114
import type { QSParserConfig } from './qs.js'
1215
import type { RequestConfig } from './request.js'
@@ -29,6 +32,15 @@ export type HttpError = {
2932
report?: (...args: any[]) => any
3033
}
3134

35+
/**
36+
* The pipeline for executing middleware during tests
37+
*/
38+
export interface TestingMiddlewarePipeline {
39+
finalHandler(handler: FinalHandler): this
40+
errorHandler(handler: ErrorHandler): this
41+
run(ctx: HttpContext): Promise<any>
42+
}
43+
3244
/**
3345
* The expression to define a status page range
3446
*/

tests/server.spec.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99

1010
import 'reflect-metadata'
1111
import supertest from 'supertest'
12+
import { Socket } from 'node:net'
1213
import { test } from '@japa/runner'
13-
import { createServer } from 'node:http'
1414
import { Emitter } from '@adonisjs/events'
1515
import type { NextFn } from '@poppinss/middleware/types'
1616
import { AppFactory } from '@adonisjs/application/factories'
17+
import { createServer, IncomingMessage, ServerResponse } from 'node:http'
1718

1819
import { Router } from '../src/router/main.js'
1920
import { HttpContext } from '../src/http_context/main.js'
@@ -990,3 +991,128 @@ test.group('Server | force content negotiation', () => {
990991
})
991992
})
992993
})
994+
995+
test.group('Server | Pipeline', () => {
996+
test('execute middleware pipeline', async ({ assert }) => {
997+
const stack: string[] = []
998+
const app = new AppFactory().create(BASE_URL, () => {})
999+
await app.init()
1000+
1001+
const server = new ServerFactory().merge({ app }).create()
1002+
1003+
class MiddlewareOne {
1004+
handle(_: any, next: NextFn) {
1005+
stack.push('middleware one')
1006+
return next()
1007+
}
1008+
}
1009+
1010+
class MiddlewareTwo {
1011+
handle(_: any, next: NextFn) {
1012+
stack.push('middleware two')
1013+
return next()
1014+
}
1015+
}
1016+
1017+
const req = new IncomingMessage(new Socket())
1018+
const res = new ServerResponse(req)
1019+
1020+
const ctx = server.createHttpContext(
1021+
server.createRequest(req, res),
1022+
server.createResponse(req, res),
1023+
app.container.createResolver()
1024+
)
1025+
1026+
await server.pipeline([MiddlewareOne, MiddlewareTwo]).run(ctx)
1027+
assert.deepEqual(stack, ['middleware one', 'middleware two'])
1028+
})
1029+
1030+
test('run final handler', async ({ assert }) => {
1031+
const stack: string[] = []
1032+
const app = new AppFactory().create(BASE_URL, () => {})
1033+
await app.init()
1034+
1035+
const server = new ServerFactory().merge({ app }).create()
1036+
1037+
class MiddlewareOne {
1038+
handle(_: any, next: NextFn) {
1039+
stack.push('middleware one')
1040+
return next()
1041+
}
1042+
}
1043+
1044+
class MiddlewareTwo {
1045+
handle(_: any, next: NextFn) {
1046+
stack.push('middleware two')
1047+
return next()
1048+
}
1049+
}
1050+
1051+
const req = new IncomingMessage(new Socket())
1052+
const res = new ServerResponse(req)
1053+
1054+
const ctx = server.createHttpContext(
1055+
server.createRequest(req, res),
1056+
server.createResponse(req, res),
1057+
app.container.createResolver()
1058+
)
1059+
1060+
await server
1061+
.pipeline([MiddlewareOne, MiddlewareTwo])
1062+
.finalHandler(async () => {
1063+
stack.push('final handler')
1064+
})
1065+
.run(ctx)
1066+
1067+
assert.deepEqual(stack, ['middleware one', 'middleware two', 'final handler'])
1068+
})
1069+
1070+
test('run error handler when error is thrown', async ({ assert }) => {
1071+
const stack: string[] = []
1072+
const app = new AppFactory().create(BASE_URL, () => {})
1073+
await app.init()
1074+
1075+
const server = new ServerFactory().merge({ app }).create()
1076+
1077+
class MiddlewareOne {
1078+
async handle(_: any, next: NextFn) {
1079+
stack.push('middleware one')
1080+
await next()
1081+
stack.push('upstream middleware one')
1082+
}
1083+
}
1084+
1085+
class MiddlewareTwo {
1086+
handle(_: any, __: NextFn) {
1087+
stack.push('middleware two')
1088+
throw new Error('Fail')
1089+
}
1090+
}
1091+
1092+
const req = new IncomingMessage(new Socket())
1093+
const res = new ServerResponse(req)
1094+
1095+
const ctx = server.createHttpContext(
1096+
server.createRequest(req, res),
1097+
server.createResponse(req, res),
1098+
app.container.createResolver()
1099+
)
1100+
1101+
await server
1102+
.pipeline([MiddlewareOne, MiddlewareTwo])
1103+
.finalHandler(async () => {
1104+
stack.push('final handler')
1105+
})
1106+
.errorHandler(async () => {
1107+
stack.push('error handler')
1108+
})
1109+
.run(ctx)
1110+
1111+
assert.deepEqual(stack, [
1112+
'middleware one',
1113+
'middleware two',
1114+
'error handler',
1115+
'upstream middleware one',
1116+
])
1117+
})
1118+
})

0 commit comments

Comments
 (0)