Skip to content

Commit

Permalink
test: add more tests
Browse files Browse the repository at this point in the history
 - small refactor improevements
 - added tests

references: #3
  • Loading branch information
refactorthis committed Apr 18, 2024
1 parent 999376e commit 79640b3
Show file tree
Hide file tree
Showing 13 changed files with 114 additions and 21 deletions.
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pnpm-lock.yaml
pnpm-lock.yaml
.sst
2 changes: 1 addition & 1 deletion examples/sst-api/packages/core/src/event.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createEventBuilder, ZodValidator } from 'sst/node/event-bus'

export const event = createEventBuilder({
// @ts-expect-error
// @ts-ignore
bus: 'bus',
validator: ZodValidator,
})
5 changes: 4 additions & 1 deletion examples/sst-api/packages/functions/src/todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,8 @@ export const remove = api({
parser: {
path: GetTodoPath,
},
handler: async ({ path }) => await Todo.remove(path.id),
handler: async ({ path }) => {
await Todo.remove(path.id)
return res.ok()
},
})
20 changes: 9 additions & 11 deletions packages/api/src/middleware/api-pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,18 @@ import httpUrlencodePathParametersParserMiddleware from '@middy/http-urlencode-p
import warmupMiddleware from '@middy/warmup'
import cloudWatchMetricsMiddleware from '@middy/cloudwatch-metrics'
import { profiler } from '@funcy/core'
import { FuncyApiOptions } from '../types'
import { FuncyApiOptions, Context } from '../types'
import validator from './validator-middleware'

export default <TResponseStruct, TEvent>(opts?: FuncyApiOptions) => {
const logger = opts?.monitoring?.logger?.() ?? console

const pipe = middy<TEvent, TResponseStruct>({
const plugin = {
...(opts?.monitoring?.enableProfiling ? profiler({ logger }) : {}),
timeoutEarlyResponse: () => {
return {
statusCode: 408,
}
},
})
timeoutEarlyResponse: () => ({ statusCode: 408 }),
}

const pipe = middy<TEvent, TResponseStruct, Error, Context>(plugin)
.use(httpEventNormalizerMiddleware())
.use(httpHeaderNormalizerMiddleware())
.use(httpUrlencodePathParametersParserMiddleware())
Expand All @@ -42,12 +40,12 @@ export default <TResponseStruct, TEvent>(opts?: FuncyApiOptions) => {
.use(httpResponseSerializerMiddleware(opts?.http?.content?.response))
.use(warmupMiddleware(opts?.function?.warmup))

if (opts?.monitoring?.cloudWatchMetrics)
pipe.use(cloudWatchMetricsMiddleware(opts?.monitoring?.cloudWatchMetrics))

if (opts?.monitoring?.logLevel === 'debug')
pipe.use(inputOutputLoggerMiddleware({ logger: logger.debug }))

if (opts?.monitoring?.cloudWatchMetrics)
pipe.use(cloudWatchMetricsMiddleware(opts?.monitoring?.cloudWatchMetrics))

if (opts?.http?.cors) pipe.use(httpCorsMiddleware(opts?.http?.cors))
if (opts?.parser) pipe.use(validator({ parser: opts?.parser, logger }))
if (opts?.function?.middleware) opts?.function?.middleware.forEach((p) => pipe.use(p))
Expand Down
7 changes: 6 additions & 1 deletion packages/api/src/middleware/validator-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ export default <TResponse, TRequest, TPath, TQuery>(opts?: {
} catch (error: any) {
if (parser.validateResponses === 'warn') {
logger.warn(new Error('WARN: Response object failed validation', { cause: error }))
} else if (parser.validateResponses === 'error') {
return
}

if (parser.validateResponses === 'error') {
throw createError(
500,
JSON.stringify({
Expand All @@ -62,6 +65,8 @@ export default <TResponse, TRequest, TPath, TQuery>(opts?: {
},
)
}

throw error
}
},
}
Expand Down
7 changes: 6 additions & 1 deletion packages/api/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { Options as CorsOptions } from '@middy/http-cors'
import { FuncyOptions } from '@funcy/core'
import { APIGatewayProxyStructuredResultV2, Context } from 'aws-lambda'
import { APIGatewayProxyStructuredResultV2, Context as LambdaContext } from 'aws-lambda'
import { ApiParser } from './parsers'
import { MetricsLogger } from '@middy/cloudwatch-metrics'

// allow void responses only if expecting void body type (if parsing a response type, then we shouldn't allow void)
export type ApiResultV2<TBody> = Omit<APIGatewayProxyStructuredResultV2, 'body'> & {
body?: TBody | undefined
}

export type Context = LambdaContext & {
metrics?: MetricsLogger
}

export type ApiHandlerFunc<TResponse, TRequest, TPath, TQuery, TAuthorizer, TEvent> = ({
request,
query,
Expand Down
1 change: 1 addition & 0 deletions packages/api/test/data/api-proxy-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export const payloadV2 = {
time: '12/Mar/2020:19:03:58 +0000',
timeEpoch: 1583348638390,
},
body: '{}',
pathParameters: {
parameter1: 'value1',
},
Expand Down
2 changes: 1 addition & 1 deletion packages/api/test/events.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect } from 'vitest'
import { describe, it } from 'vitest'
import { api } from '../src/api'
import * as events from './data/api-proxy-events'
import { ctx } from './data/lambda-context'
Expand Down
2 changes: 1 addition & 1 deletion packages/api/test/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('Handlers', () => {
expect(response.statusCode).toBe(200)
})

// supporting void makes the interface harder to use, so skipping this.
// supporting void makes the interface harder to use, so removing pending decision.
// it.skip('should support a void response', async () => {
// const fn = api({ handler: () => {} })
// const response = await fn(events.payloadV2 as any, ctx())
Expand Down
9 changes: 9 additions & 0 deletions packages/api/test/mocks/logger.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Logger } from '@funcy/core/src/types'
import { vi } from 'vitest'

export const logger = (): Logger => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
})
4 changes: 2 additions & 2 deletions packages/api/test/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ describe('options', () => {
it('should merge & override defaults with api level options if specified', () => {
const api = createApi({
monitoring: {
logLevel: 'trace',
logLevel: 'debug',
},
http: {
cors: {
origin: 'myorigin.com',
},
},
})
expect(api.defaultOptions.monitoring?.logLevel).toBe('trace')
expect(api.defaultOptions.monitoring?.logLevel).toBe('debug')
expect(api.defaultOptions.http?.cors?.origin).toBe('myorigin.com')
})

Expand Down
71 changes: 71 additions & 0 deletions packages/api/test/pipeline.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, expect, vi } from 'vitest'
import { api, res } from '../'
import * as events from './data/api-proxy-events'
import { ctx } from './data/lambda-context'
import { logger } from './mocks/logger.mock'

describe('pipeline', () => {
it('should enable cloudwatch metrics if specified', () => {
const event: any = { ...events.payloadV2 }
const fn = api({
monitoring: {
cloudWatchMetrics: {
namespace: 'my-namespace',
},
},
handler: vi.fn<any>(({ context }) => {
expect(context.metrics).toBeDefined()
return res.ok()
}),
})

fn(event, ctx())
})

it('should enable full input/output logging if debug mode', async () => {
const event: any = { ...events.payloadV2 }
const log = logger()
const fn = api({
monitoring: {
logger: () => log,
logLevel: 'debug',
},
handler: () => res.ok(),
})

await fn(event, ctx())
expect(log.debug).toHaveBeenCalled()
})

it('should add extra middleware to the pipeline if specified', async () => {
const event: any = { ...events.payloadV2 }
const middleware = { before: vi.fn() }
const fn = api({
function: {
middleware: [middleware],
},
handler: () => res.ok(),
})

await fn(event, ctx())
expect(middleware.before).toHaveBeenCalled()
})

it('should enable CORS middleware if specified', async () => {
const event: any = { ...events.payloadV2 }
const disabled = api({
handler: () => res.ok(),
})

const r1 = await disabled(event, ctx())
expect(r1.headers?.['Access-Control-Allow-Origin']).toBeUndefined()

const enabled = api({
http: { cors: { origin: 'web.mysite.com' } },
handler: () => res.ok(),
})

const r2 = await enabled(event, ctx())
expect(r2.headers?.['Access-Control-Allow-Origin']).toBe('web.mysite.com')
})
})
2 changes: 1 addition & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export interface FuncyOptions<TEvent, TResponse> {
* Error handler
* // TODO
*/
onError: (error: any) => void
onError?: (error: any) => void
}
}

Expand Down

0 comments on commit 79640b3

Please sign in to comment.