diff --git a/src/config/commandOptions.js b/src/config/commandOptions.js index 2a899a8a1..082686459 100644 --- a/src/config/commandOptions.js +++ b/src/config/commandOptions.js @@ -134,9 +134,10 @@ export default { type: 'boolean', usage: 'Uses docker for node/python/ruby/provided', }, - useWorkerThreads: { + useInProcess: { type: 'boolean', - usage: "Use 'worker threads' to run handlers.", + usage: + "Run handlers in the same process as 'serverless-offline'. NOTE: This can cause memory leaks and is not recommended. This option will likely be removed in future versions.", }, webSocketHardTimeout: { type: 'string', diff --git a/src/config/defaultOptions.js b/src/config/defaultOptions.js index 7468d8cd2..fc3170e7e 100644 --- a/src/config/defaultOptions.js +++ b/src/config/defaultOptions.js @@ -30,7 +30,7 @@ export default { resourceRoutes: false, useChildProcesses: false, useDocker: false, - useWorkerThreads: false, + useInProcess: false, webSocketHardTimeout: 7200, webSocketIdleTimeout: 600, websocketPort: 3001, diff --git a/src/lambda/handler-runner/HandlerRunner.js b/src/lambda/handler-runner/HandlerRunner.js index 5ba403209..6678c5f0a 100644 --- a/src/lambda/handler-runner/HandlerRunner.js +++ b/src/lambda/handler-runner/HandlerRunner.js @@ -23,7 +23,7 @@ export default class HandlerRunner { } async #loadRunner() { - const { useDocker, useChildProcesses, useWorkerThreads, allowCache } = + const { allowCache, useChildProcesses, useDocker, useInProcess } = this.#options const { functionKey, handlerName, handlerPath, runtime, timeout } = @@ -67,26 +67,26 @@ export default class HandlerRunner { return new ChildProcessRunner(this.#funOptions, this.#env, allowCache) } - if (useWorkerThreads) { - const { default: WorkerThreadRunner } = await import( - './worker-thread-runner/index.js' + if (useInProcess) { + const { default: InProcessRunner } = await import( + './in-process-runner/index.js' ) - return new WorkerThreadRunner(this.#funOptions, this.#env, allowCache) + return new InProcessRunner( + functionKey, + handlerPath, + handlerName, + this.#env, + timeout, + allowCache, + ) } - const { default: InProcessRunner } = await import( - './in-process-runner/index.js' + const { default: WorkerThreadRunner } = await import( + './worker-thread-runner/index.js' ) - return new InProcessRunner( - functionKey, - handlerPath, - handlerName, - this.#env, - timeout, - allowCache, - ) + return new WorkerThreadRunner(this.#funOptions, this.#env, allowCache) } if (supportedGo.has(runtime)) { diff --git a/src/lambda/handler-runner/in-process-runner/InProcessRunner.js b/src/lambda/handler-runner/in-process-runner/InProcessRunner.js index 8f3d27a39..d50a7d7b7 100644 --- a/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +++ b/src/lambda/handler-runner/in-process-runner/InProcessRunner.js @@ -41,7 +41,7 @@ export default class InProcessRunner { if (this.#memoryLeakWarning) { log.warning() log.warning( - `Running 'serverless-offline' and the handlers side-by-side in the same process with reloading enabled will cause memory leaks!`, + `Running the handlers in-process with 'serverless-offline' with reloading enabled is not recommended as it causes memory leaks!`, ) log.warning() this.#memoryLeakWarning = false diff --git a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js index 578c28060..a9cabb034 100644 --- a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +++ b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js @@ -40,7 +40,14 @@ export default class WorkerThreadRunner { const { port1, port2 } = new MessageChannel() port1 - .on('message', res) + .on('message', (value) => { + if (value instanceof Error) { + rej(value) + return + } + + res(value) + }) // emitted if the worker thread throws an uncaught exception. // In that case, the worker will be terminated. .on('error', rej) diff --git a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js index 189a081af..f488171be 100644 --- a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +++ b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js @@ -17,7 +17,13 @@ parentPort.on('message', async (messageData) => { allowCache, ) - const result = await inProcessRunner.run(event, context) + let result + + try { + result = await inProcessRunner.run(event, context) + } catch (err) { + port.postMessage(err) + } // TODO check serializeability (contains function, symbol etc) port.postMessage(result) diff --git a/tests/old-unit/offline.test.js b/tests/old-unit/offline.test.js index f5559245c..ebe6b91ca 100644 --- a/tests/old-unit/offline.test.js +++ b/tests/old-unit/offline.test.js @@ -7,14 +7,21 @@ const { parse, stringify } = JSON describe('Offline', () => { describe('with a non existing route', () => { let offline + let server beforeEach(async () => { // Creates offline test server with no function - offline = await new OfflineBuilder(new ServerlessBuilder()).toObject() + offline = new OfflineBuilder(new ServerlessBuilder()) + + server = await offline.toObject() + }) + + afterEach(async () => { + await offline.end(true) }) it('should return 404 status code', async () => { - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', url: '/dev/magic', }) @@ -25,10 +32,11 @@ describe('Offline', () => { describe('with private function', () => { let offline + let server const validToken = 'valid-token' beforeEach(async () => { - offline = await new OfflineBuilder(new ServerlessBuilder(), { + offline = new OfflineBuilder(new ServerlessBuilder(), { apiKey: validToken, }) .addFunctionConfig('fn2', { @@ -44,11 +52,16 @@ describe('Offline', () => { handler: 'tests/old-unit/fixtures/handler.basicAuthentication1', }) .addApiKeys(['token']) - .toObject() + + server = await offline.toObject() + }) + + afterEach(async () => { + await offline.end(true) }) it('should return bad request with no token', async () => { - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', url: '/dev/fn2', }) @@ -64,7 +77,7 @@ describe('Offline', () => { }) it('should return forbidden if token is wrong', async () => { - const res = await offline.inject({ + const res = await server.inject({ headers: { 'x-api-key': 'random string', }, @@ -83,7 +96,7 @@ describe('Offline', () => { }) it('should return the function executed correctly', async () => { - const res = await offline.inject({ + const res = await server.inject({ headers: { 'x-api-key': validToken, }, @@ -103,10 +116,11 @@ describe('Offline', () => { describe('with private function and noAuth option set', () => { let offline + let server const validToken = 'valid-token' beforeEach(async () => { - offline = await new OfflineBuilder(new ServerlessBuilder(), { + offline = new OfflineBuilder(new ServerlessBuilder(), { apiKey: validToken, noAuth: true, }) @@ -123,11 +137,16 @@ describe('Offline', () => { handler: 'tests/old-unit/fixtures/handler.basicAuthentication2', }) .addApiKeys(['token']) - .toObject() + + server = await offline.toObject() + }) + + afterEach(async () => { + await offline.end(true) }) it('should execute the function correctly if no API key is provided', async () => { - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', url: '/dev/fn3', }) @@ -136,7 +155,7 @@ describe('Offline', () => { }) it('should execute the function correctly if API key is provided', async () => { - const res = await offline.inject({ + const res = await server.inject({ headers: { 'x-api-key': validToken, }, @@ -150,118 +169,124 @@ describe('Offline', () => { describe('lambda integration', () => { it('should use event defined response template and headers', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('index', { - events: [ - { - http: { - integration: 'lambda', - method: 'GET', - path: 'index', - response: { - headers: { - 'Content-Type': "'text/html'", - }, - template: "$input.path('$')", + const offline = new OfflineBuilder().addFunctionConfig('index', { + events: [ + { + http: { + integration: 'lambda', + method: 'GET', + path: 'index', + response: { + headers: { + 'Content-Type': "'text/html'", }, + template: "$input.path('$')", }, }, - ], - handler: 'tests/old-unit/fixtures/handler.usersIndex1', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.usersIndex1', + }) + + const server = await offline.toObject() - const res = await offline.inject('/dev/index') + const res = await server.inject('/dev/index') assert.strictEqual( res.headers['content-type'], 'text/html; charset=utf-8', ) assert.strictEqual(res.statusCode, 200) + + await offline.end(true) }) describe('error handling', () => { it('should set the status code to 502 when no [xxx] is present', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('index', { - events: [ - { - http: { - integration: 'lambda', - method: 'GET', - path: 'index', - response: { - headers: { - 'Content-Type': "'text/html'", - }, - template: "$input.path('$')", + const offline = new OfflineBuilder().addFunctionConfig('index', { + events: [ + { + http: { + integration: 'lambda', + method: 'GET', + path: 'index', + response: { + headers: { + 'Content-Type': "'text/html'", }, + template: "$input.path('$')", }, }, - ], - handler: 'tests/old-unit/fixtures/handler.usersIndex2', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.usersIndex2', + }) + + const server = await offline.toObject() - const res = await offline.inject('/dev/index') + const res = await server.inject('/dev/index') assert.strictEqual( res.headers['content-type'], 'text/html; charset=utf-8', ) assert.strictEqual(res.statusCode, 502) + + await offline.end(true) }) it('should set the status code to 401 when [401] is the prefix of the error message', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('index', { - events: [ - { - http: { - integration: 'lambda', - method: 'GET', - path: 'index', - response: { - headers: { - 'Content-Type': "'text/html'", - }, - template: "$input.path('$')", + const offline = new OfflineBuilder().addFunctionConfig('index', { + events: [ + { + http: { + integration: 'lambda', + method: 'GET', + path: 'index', + response: { + headers: { + 'Content-Type': "'text/html'", }, + template: "$input.path('$')", }, }, - ], - handler: 'tests/old-unit/fixtures/handler.usersIndex3', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.usersIndex3', + }) + + const server = await offline.toObject() - const res = await offline.inject('/dev/index') + const res = await server.inject('/dev/index') assert.strictEqual( res.headers['content-type'], 'text/html; charset=utf-8', ) assert.strictEqual(res.statusCode, 401) + + await offline.end(true) }) }) }) describe('lambda-proxy integration', () => { it('should accept and return application/json content type by default', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn1', { - events: [ - { - http: { - method: 'GET', - path: 'fn1', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn1', { + events: [ + { + http: { + method: 'GET', + path: 'fn1', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn1', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn1', + }) + + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', payload: { data: 'data', @@ -273,24 +298,26 @@ describe('Offline', () => { res.headers['content-type'], 'application/json; charset=utf-8', ) + + await offline.end(true) }) it('should accept and return application/json content type', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn2', { - events: [ - { - http: { - method: 'GET', - path: 'fn2', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn2', { + events: [ + { + http: { + method: 'GET', + path: 'fn2', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn2', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn2', + }) - const res = await offline.inject({ + const server = await offline.toObject() + + const res = await server.inject({ headers: { 'content-type': 'application/json', }, @@ -305,24 +332,26 @@ describe('Offline', () => { res.headers['content-type'], 'application/json; charset=utf-8', ) + + await offline.end(true) }) it('should accept and return custom content type', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn3', { - events: [ - { - http: { - method: 'GET', - path: 'fn3', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn3', { + events: [ + { + http: { + method: 'GET', + path: 'fn3', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn3', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn3', + }) + + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ headers: { 'content-type': 'application/vnd.api+json', }, @@ -337,24 +366,26 @@ describe('Offline', () => { res.headers['content-type'], 'application/vnd.api+json', ) + + await offline.end(true) }) it('should return application/json content type by default', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn4', { - events: [ - { - http: { - method: 'GET', - path: 'fn4', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn4', { + events: [ + { + http: { + method: 'GET', + path: 'fn4', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn4', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn4', + }) + + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', url: '/dev/fn4', }) @@ -363,52 +394,58 @@ describe('Offline', () => { res.headers['content-type'], 'application/json; charset=utf-8', ) + + await offline.end(true) }) it('should work with trailing slashes path', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn5', { - events: [ - { - http: { - method: 'GET', - path: 'fn5', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn5', { + events: [ + { + http: { + method: 'GET', + path: 'fn5', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn5', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn5', + }) - const res = await offline.inject({ + const server = await offline.toObject() + + const res = await server.inject({ method: 'GET', url: '/dev/fn5', }) assert.strictEqual(res.statusCode, 201) + + await offline.end(true) }) it('should return the expected status code', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn6', { - events: [ - { - http: { - method: 'GET', - path: 'fn6', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn6', { + events: [ + { + http: { + method: 'GET', + path: 'fn6', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn6', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn6', + }) - const res = await offline.inject({ + const server = await offline.toObject() + + const res = await server.inject({ method: 'GET', url: '/dev/fn6', }) assert.strictEqual(res.statusCode, 201) + + await offline.end(true) }) // TODO FIXME this test does not do anything, assertion missing @@ -432,54 +469,59 @@ describe('Offline', () => { // }) it('should return correctly set multiple set-cookie headers', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn7', { - events: [ - { - http: { - method: 'GET', - path: 'fn7', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn7', { + events: [ + { + http: { + method: 'GET', + path: 'fn7', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn7', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn7', + }) + + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', url: '/dev/fn7', }) assert.deepStrictEqual(res.headers['set-cookie'], ['foo=bar', 'floo=baz']) + + await offline.end(true) }) }) describe('with catch-all route', () => { it('should match arbitary route', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('test', { - events: [ - { - http: { - method: 'GET', - path: 'test/{stuff+}', - }, + const offline = new OfflineBuilder().addFunctionConfig('test', { + events: [ + { + http: { + method: 'GET', + path: 'test/{stuff+}', }, - ], - handler: 'tests/old-unit/fixtures/handler.test', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.test', + }) + + const server = await offline.toObject() - const res = await offline.inject('/dev/test/some/matching/route') + const res = await server.inject('/dev/test/some/matching/route') assert.strictEqual(res.statusCode, 200) assert.strictEqual(res.payload, 'Hello') + + await offline.end(true) }) }) describe('does not mangle payload', () => { let offline + let server const rawBody = `{ \t"type": "notification_event", @@ -503,8 +545,9 @@ describe('Offline', () => { }` beforeEach(async () => { - offline = await new OfflineBuilder(new ServerlessBuilder()) - .addFunctionConfig('rawJsonBody', { + offline = new OfflineBuilder(new ServerlessBuilder()).addFunctionConfig( + 'rawJsonBody', + { events: [ { http: { @@ -514,12 +557,17 @@ describe('Offline', () => { }, ], handler: 'tests/old-unit/fixtures/handler.rawJsonBody', - }) - .toObject() + }, + ) + server = await offline.toObject() + }) + + afterEach(async () => { + await offline.end(true) }) it.skip('should return that the JSON was not mangled', async () => { - const res = await offline.inject({ + const res = await server.inject({ method: 'POST', payload: rawBody, url: '/dev/raw-json-body', @@ -535,7 +583,7 @@ describe('Offline', () => { }) it.skip('should return that the JSON was not mangled with an application/json type', async () => { - const res = await offline.inject({ + const res = await server.inject({ headers: { 'content-type': 'application/json', }, @@ -567,23 +615,23 @@ describe('Offline', () => { } it('should support handler returning Promise', async () => { - const offline = await new OfflineBuilder( + const offline = new OfflineBuilder( new ServerlessBuilder(serverless), - ) - .addFunctionConfig('promise', { - events: [ - { - http: { - method: 'GET', - path: 'promise', - }, + ).addFunctionConfig('promise', { + events: [ + { + http: { + method: 'GET', + path: 'promise', }, - ], - handler: 'tests/old-unit/fixtures/handler.promise', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.promise', + }) + + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', payload: { data: 'input', @@ -597,26 +645,28 @@ describe('Offline', () => { ) assert.strictEqual(res.statusCode, 200) assert.strictEqual(res.payload, '{"message":"Hello World"}') + + await offline.end(true) }) it('should support handler returning Promise that defers', async () => { - const offline = await new OfflineBuilder( + const offline = new OfflineBuilder( new ServerlessBuilder(serverless), - ) - .addFunctionConfig('promiseDeferred', { - events: [ - { - http: { - method: 'GET', - path: 'promise-deferred', - }, + ).addFunctionConfig('promiseDeferred', { + events: [ + { + http: { + method: 'GET', + path: 'promise-deferred', }, - ], - handler: 'tests/old-unit/fixtures/handler.promiseDeferred', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.promiseDeferred', + }) - const res = await offline.inject({ + const server = await offline.toObject() + + const res = await server.inject({ method: 'GET', payload: { data: 'input', @@ -630,26 +680,28 @@ describe('Offline', () => { ) assert.strictEqual(res.statusCode, 200) assert.strictEqual(res.payload, '{"message":"Hello World"}') + + await offline.end(true) }) it('should support handler that defers and uses done()', async () => { - const offline = await new OfflineBuilder( + const offline = new OfflineBuilder( new ServerlessBuilder(serverless), - ) - .addFunctionConfig('doneDeferred', { - events: [ - { - http: { - method: 'GET', - path: 'done-deferred', - }, + ).addFunctionConfig('doneDeferred', { + events: [ + { + http: { + method: 'GET', + path: 'done-deferred', }, - ], - handler: 'tests/old-unit/fixtures/handler.doneDeferred', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.doneDeferred', + }) + + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', payload: { data: 'input', @@ -663,26 +715,28 @@ describe('Offline', () => { ) assert.strictEqual(res.statusCode, 200) assert.strictEqual(res.payload, '{"message":"Hello World"}') + + await offline.end(true) }) it('should support handler that throws and uses done()', async () => { - const offline = await new OfflineBuilder( + const offline = new OfflineBuilder( new ServerlessBuilder(serverless), - ) - .addFunctionConfig('throwDone', { - events: [ - { - http: { - method: 'GET', - path: 'throw-done', - }, + ).addFunctionConfig('throwDone', { + events: [ + { + http: { + method: 'GET', + path: 'throw-done', }, - ], - handler: 'tests/old-unit/fixtures/handler.throwDone', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.throwDone', + }) + + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', payload: { data: 'input', @@ -695,26 +749,27 @@ describe('Offline', () => { 'application/json; charset=utf-8', ) assert.strictEqual(res.statusCode, 502) + + await offline.end(true) }) it('should support handler using async function', async () => { - const offline = await new OfflineBuilder( + const offline = new OfflineBuilder( new ServerlessBuilder(serverless), - ) - .addFunctionConfig('asyncFunction', { - events: [ - { - http: { - method: 'GET', - path: 'async-function', - }, + ).addFunctionConfig('asyncFunction', { + events: [ + { + http: { + method: 'GET', + path: 'async-function', }, - ], - handler: 'tests/old-unit/fixtures/handler.asyncFunction', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.asyncFunction', + }) + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', payload: { data: 'input', @@ -728,26 +783,28 @@ describe('Offline', () => { ) assert.strictEqual(res.statusCode, 200) assert.strictEqual(res.payload, '{"message":"Hello World"}') + + await offline.end(true) }) it('should support handler that uses async function that throws', async () => { - const offline = await new OfflineBuilder( + const offline = new OfflineBuilder( new ServerlessBuilder(serverless), - ) - .addFunctionConfig('asyncFunctionThrows', { - events: [ - { - http: { - method: 'GET', - path: 'async-function-throws', - }, + ).addFunctionConfig('asyncFunctionThrows', { + events: [ + { + http: { + method: 'GET', + path: 'async-function-throws', }, - ], - handler: 'tests/old-unit/fixtures/handler.asyncFunctionThrows', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.asyncFunctionThrows', + }) + + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'GET', payload: { data: 'input', @@ -760,165 +817,171 @@ describe('Offline', () => { 'application/json; charset=utf-8', ) assert.strictEqual(res.statusCode, 502) + + await offline.end(true) }) }) describe('with HEAD support', () => { it('should skip HEAD route mapping and return 404 when requested', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('foo', { - events: [ - { - http: { - method: 'HEAD', - path: 'fn8', - }, + const offline = new OfflineBuilder().addFunctionConfig('foo', { + events: [ + { + http: { + method: 'HEAD', + path: 'fn8', }, - ], - handler: 'tests/old-unit/fixtures/handler.foo', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.foo', + }) + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'HEAD', url: '/dev/foo', }) assert.strictEqual(res.statusCode, 404) + + await offline.end(true) }) it('should use GET route for HEAD requests, if exists', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn8', { - events: [ - { - http: { - method: 'GET', - path: 'fn8', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn8', { + events: [ + { + http: { + method: 'GET', + path: 'fn8', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn8', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn8', + }) + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ method: 'HEAD', url: '/dev/fn8', }) assert.strictEqual(res.statusCode, 204) + + await offline.end(true) }) }) describe('static headers', () => { it('are returned if defined in lambda integration', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('headers', { - events: [ - { - http: { - integration: 'lambda', - method: 'GET', - path: 'headers', - response: { - headers: { - 'custom-header-1': "'first value'", - 'Custom-Header-2': "'Second Value'", - 'custom-header-3': "'third's value'", - }, + const offline = new OfflineBuilder().addFunctionConfig('headers', { + events: [ + { + http: { + integration: 'lambda', + method: 'GET', + path: 'headers', + response: { + headers: { + 'custom-header-1': "'first value'", + 'Custom-Header-2': "'Second Value'", + 'custom-header-3': "'third's value'", }, }, }, - ], - handler: 'tests/old-unit/fixtures/handler.headers', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.headers', + }) + const server = await offline.toObject() - const res = await offline.inject('/dev/headers') + const res = await server.inject('/dev/headers') assert.strictEqual(res.statusCode, 200) assert.strictEqual(res.headers['custom-header-1'], 'first value') assert.strictEqual(res.headers['custom-header-2'], 'Second Value') assert.strictEqual(res.headers['custom-header-3'], "third's value") + + await offline.end(true) }) it('are not returned if not double-quoted strings in lambda integration', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('headers', { - events: [ - { - http: { - integration: 'lambda', - method: 'GET', - path: 'headers', - response: { - headers: { - 'custom-header-1': 'first value', - 'Custom-Header-2': true, - }, + const offline = new OfflineBuilder().addFunctionConfig('headers', { + events: [ + { + http: { + integration: 'lambda', + method: 'GET', + path: 'headers', + response: { + headers: { + 'custom-header-1': 'first value', + 'Custom-Header-2': true, }, }, }, - ], - handler: 'tests/old-unit/fixtures/handler.headers', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.headers', + }) + const server = await offline.toObject() - const res = await offline.inject('/dev/headers') + const res = await server.inject('/dev/headers') assert.strictEqual(res.statusCode, 200) assert.strictEqual(res.headers['custom-header-1'], undefined) assert.strictEqual(res.headers['custom-header-2'], undefined) + + await offline.end(true) }) it('are not returned if defined in non-lambda integration', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('headers', { - events: [ - { - http: { - integration: 'other', - method: 'GET', - path: 'headers', - response: { - headers: { - 'custom-header-1': "'first value'", - 'Custom-Header-2': "'Second Value'", - }, + const offline = new OfflineBuilder().addFunctionConfig('headers', { + events: [ + { + http: { + integration: 'other', + method: 'GET', + path: 'headers', + response: { + headers: { + 'custom-header-1': "'first value'", + 'Custom-Header-2': "'Second Value'", }, }, }, - ], - handler: 'tests/old-unit/fixtures/handler.headers', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.headers', + }) + const server = await offline.toObject() - const res = await offline.inject('/dev/headers') + const res = await server.inject('/dev/headers') assert.strictEqual(res.statusCode, 204) assert.strictEqual(res.headers['custom-header-1'], undefined) assert.strictEqual(res.headers['custom-header-2'], undefined) + + await offline.end(true) }) }) describe('disable cookie validation', () => { it.skip('should return bad reqeust by default if invalid cookies are passed by the request', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('cookie', { - events: [ - { - http: { - method: 'GET', - path: 'cookie', - }, + const offline = new OfflineBuilder().addFunctionConfig('cookie', { + events: [ + { + http: { + method: 'GET', + path: 'cookie', }, - ], - handler: 'tests/old-unit/fixtures/handler.cookie', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.cookie', + }) + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ headers: { Cookie: 'a.strange.cookie.with.newline.at.the.end=yummie123utuiwi-32432fe3-f3e2e32\n', @@ -928,26 +991,27 @@ describe('Offline', () => { }) assert.strictEqual(res.statusCode, 400) + + await offline.end(true) }) it('should return 200 if the "disableCookieValidation"-flag is set', async () => { - const offline = await new OfflineBuilder(new ServerlessBuilder(), { + const offline = new OfflineBuilder(new ServerlessBuilder(), { disableCookieValidation: true, - }) - .addFunctionConfig('cookie', { - events: [ - { - http: { - method: 'GET', - path: 'cookie', - }, + }).addFunctionConfig('cookie', { + events: [ + { + http: { + method: 'GET', + path: 'cookie', }, - ], - handler: 'tests/old-unit/fixtures/handler.cookie', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.cookie', + }) + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ headers: { Cookie: 'a.strange.cookie.with.newline.at.the.end=yummie123utuiwi-32432fe3-f3e2e32\n', @@ -957,26 +1021,27 @@ describe('Offline', () => { }) assert.strictEqual(res.statusCode, 204) + + await offline.end(true) }) }) describe('check cookie status', () => { it('check for isHttpOnly off', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn9', { - events: [ - { - http: { - method: 'GET', - path: 'fn9', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn9', { + events: [ + { + http: { + method: 'GET', + path: 'fn9', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn9', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn9', + }) + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ headers: {}, method: 'GET', url: '/dev/fn9', @@ -985,24 +1050,25 @@ describe('Offline', () => { res.headers['set-cookie'].forEach((v) => assert.strictEqual(v.match(/httponly/i), null), ) + + await offline.end(true) }) it('check for isSecure off', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn10', { - events: [ - { - http: { - method: 'GET', - path: 'fn10', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn10', { + events: [ + { + http: { + method: 'GET', + path: 'fn10', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn10', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn10', + }) + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ headers: {}, method: 'GET', url: '/dev/fn10', @@ -1011,24 +1077,25 @@ describe('Offline', () => { res.headers['set-cookie'].forEach((v) => assert.strictEqual(v.match(/secure/i), null), ) + + await offline.end(true) }) it('check for isSameSite off', async () => { - const offline = await new OfflineBuilder() - .addFunctionConfig('fn11', { - events: [ - { - http: { - method: 'GET', - path: 'fn11', - }, + const offline = new OfflineBuilder().addFunctionConfig('fn11', { + events: [ + { + http: { + method: 'GET', + path: 'fn11', }, - ], - handler: 'tests/old-unit/fixtures/handler.fn11', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.fn11', + }) + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ headers: {}, method: 'GET', url: '/dev/fn11', @@ -1037,6 +1104,8 @@ describe('Offline', () => { res.headers['set-cookie'].forEach((v) => assert.strictEqual(v.match(/samesite/i), null), ) + + await offline.end(true) }) }) @@ -1072,35 +1141,39 @@ describe('Offline', () => { }) it('proxies query strings', async () => { - const offline = await new OfflineBuilder(serviceBuilder, { + const offline = new OfflineBuilder(serviceBuilder, { resourceRoutes: true, - }).toObject() + }) + + const server = await offline.toObject() - const res = await offline.inject('/dev/echo/foo?bar=baz') + const res = await server.inject('/dev/echo/foo?bar=baz') const result = parse(res.result) assert.strictEqual(result.queryString.bar, 'baz') + + await offline.end(true) }) describe('disable cookie validation', () => { it.skip('should return bad request by default if invalid cookies are passed by the request', async () => { - const offline = await new OfflineBuilder(serviceBuilder, { + const offline = new OfflineBuilder(serviceBuilder, { resourceRoutes: true, - }) - .addFunctionConfig('cookie', { - events: [ - { - http: { - method: 'GET', - path: 'cookie', - }, + }).addFunctionConfig('cookie', { + events: [ + { + http: { + method: 'GET', + path: 'cookie', }, - ], - handler: 'tests/old-unit/fixtures/handler.cookie', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.cookie', + }) + + const server = await offline.toObject() - const res = await offline.inject({ + const res = await server.inject({ headers: { Cookie: 'a.strange.cookie.with.newline.at.the.end=yummie123utuiwi-32432fe3-f3e2e32\n', @@ -1110,27 +1183,29 @@ describe('Offline', () => { }) assert.strictEqual(res.statusCode, 400) + + await offline.end(true) }) it('should return 200 if the "disableCookieValidation"-flag is set', async () => { - const offline = await new OfflineBuilder(serviceBuilder, { + const offline = new OfflineBuilder(serviceBuilder, { disableCookieValidation: true, resourceRoutes: true, - }) - .addFunctionConfig('cookie', { - events: [ - { - http: { - method: 'GET', - path: 'cookie', - }, + }).addFunctionConfig('cookie', { + events: [ + { + http: { + method: 'GET', + path: 'cookie', }, - ], - handler: 'tests/old-unit/fixtures/handler.cookie', - }) - .toObject() + }, + ], + handler: 'tests/old-unit/fixtures/handler.cookie', + }) - const res = await offline.inject({ + const server = await offline.toObject() + + const res = await server.inject({ headers: { Cookie: 'a.strange.cookie.with.newline.at.the.end=yummie123utuiwi-32432fe3-f3e2e32\n', @@ -1140,6 +1215,8 @@ describe('Offline', () => { }) assert.strictEqual(res.statusCode, 204) + + await offline.end(true) }) }) }) diff --git a/tests/old-unit/support/OfflineBuilder.js b/tests/old-unit/support/OfflineBuilder.js index de5734eaf..4214556ae 100644 --- a/tests/old-unit/support/OfflineBuilder.js +++ b/tests/old-unit/support/OfflineBuilder.js @@ -10,11 +10,25 @@ export default class OfflineBuilder { #serverlessBuilder = null + #serverlessOffline = null + constructor(serverlessBuilder, options) { this.#options = options ?? {} this.#serverlessBuilder = serverlessBuilder ?? new ServerlessBuilder() } + addApiKeys(keys) { + this.#serverlessBuilder.addApiKeys(keys) + + return this + } + + addCustom(prop, value) { + this.#serverlessBuilder.addCustom(prop, value) + + return this + } + addFunctionConfig(functionKey, functionConfig, handler) { this.#serverlessBuilder.addFunction(functionKey, functionConfig) @@ -31,30 +45,22 @@ export default class OfflineBuilder { return this } - addCustom(prop, value) { - this.#serverlessBuilder.addCustom(prop, value) - - return this - } - - addApiKeys(keys) { - this.#serverlessBuilder.addApiKeys(keys) - - return this - } - async toObject() { - const serverlessOffline = new ServerlessOffline( + this.#serverlessOffline = new ServerlessOffline( this.#serverlessBuilder.toObject(), this.#options, ) - serverlessOffline._mergeOptions() + this.#serverlessOffline._mergeOptions() + + const { httpEvents, lambdas } = this.#serverlessOffline._getEvents() + await this.#serverlessOffline._createLambda(lambdas, true) + await this.#serverlessOffline._createHttp(httpEvents, true) - const { httpEvents, lambdas } = serverlessOffline._getEvents() - await serverlessOffline._createLambda(lambdas, true) - await serverlessOffline._createHttp(httpEvents, true) + return this.#serverlessOffline.getApiGatewayServer() + } - return serverlessOffline.getApiGatewayServer() + end(skipExit) { + return this.#serverlessOffline.end(skipExit) } } diff --git a/tests/old-unit/support/RequestBuilder.js b/tests/old-unit/support/RequestBuilder.js index eae293172..fc8feabd6 100644 --- a/tests/old-unit/support/RequestBuilder.js +++ b/tests/old-unit/support/RequestBuilder.js @@ -25,17 +25,17 @@ export default class RequestBuilder { } } - addHeader(key, value) { - this.request.headers[key] = value - this.request.raw.req.rawHeaders.push(key, value) - } - addBody(body) { this.request.payload = body // The rawPayload would normally be the string version of the given body this.request.rawPayload = stringify(body) } + addHeader(key, value) { + this.request.headers[key] = value + this.request.raw.req.rawHeaders.push(key, value) + } + addParam(key, value) { this.request.params[key] = value }