mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 18:35:07 +03:00
214 lines
7.3 KiB
TypeScript
214 lines
7.3 KiB
TypeScript
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import { toWebHandler } from './api'
|
|
|
|
const createRequest = (body?: string, contentType = 'application/json') =>
|
|
new Request('http://localhost/api/test', {
|
|
method: 'POST',
|
|
headers: { 'content-type': contentType },
|
|
body,
|
|
})
|
|
|
|
describe('toWebHandler request parsing', () => {
|
|
it.each(
|
|
['application/json', 'application/ld+json', 'Application/JSON; charset=utf-8'].flatMap(
|
|
(contentType) =>
|
|
['{', '[1,', 'undefined', ' ', '{"private":"value",}'].map((body) => ({
|
|
contentType,
|
|
body,
|
|
}))
|
|
)
|
|
)(
|
|
'returns 400 without calling the handler for malformed $contentType: $body',
|
|
async ({ body, contentType }) => {
|
|
const handler = vi.fn()
|
|
const response = await toWebHandler(handler)({ request: createRequest(body, contentType) })
|
|
|
|
expect(response.status).toBe(400)
|
|
expect(await response.text()).toBe('Invalid JSON')
|
|
expect(handler).not.toHaveBeenCalled()
|
|
}
|
|
)
|
|
|
|
it.each<[string | undefined, unknown]>([
|
|
['{"query":"select 1"}', { query: 'select 1' }],
|
|
['[1,"two",null]', [1, 'two', null]],
|
|
['"hello"', 'hello'],
|
|
['42', 42],
|
|
['true', true],
|
|
['false', false],
|
|
['null', null],
|
|
['', {}],
|
|
[undefined, {}],
|
|
])('passes valid JSON %s to the handler', async (body, expected) => {
|
|
const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => {
|
|
expect(req.body).toEqual(expected)
|
|
res.status(201).setHeader('x-handler', 'called')
|
|
res.json({ success: true })
|
|
})
|
|
|
|
const response = await toWebHandler(handler)({ request: createRequest(body) })
|
|
|
|
expect(handler).toHaveBeenCalledOnce()
|
|
expect(response.status).toBe(201)
|
|
expect(response.headers.get('x-handler')).toBe('called')
|
|
expect(response.headers.get('content-type')).toBe('application/json')
|
|
expect(await response.json()).toEqual({ success: true })
|
|
})
|
|
|
|
it.each(['application/json; charset=utf-8', 'application/ld+json', 'Application/JSON'])(
|
|
'accepts JSON content type %s',
|
|
async (contentType) => {
|
|
const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => res.json(req.body))
|
|
const response = await toWebHandler(handler)({
|
|
request: createRequest('{"name":"café"}', contentType),
|
|
})
|
|
|
|
expect(await response.json()).toEqual({ name: 'café' })
|
|
}
|
|
)
|
|
|
|
it.each<[string, string, unknown]>([
|
|
[
|
|
'name=hello+world&value=%26%3D',
|
|
'application/x-www-form-urlencoded',
|
|
{
|
|
name: 'hello world',
|
|
value: '&=',
|
|
},
|
|
],
|
|
['{', 'text/plain', '{'],
|
|
['{', 'text/plain; note="application/json"', '{'],
|
|
['{', 'application/jsonx', '{'],
|
|
])('preserves non-JSON request bodies', async (body, contentType, expected) => {
|
|
const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => {
|
|
expect(req.body).toEqual(expected)
|
|
res.end('ok')
|
|
})
|
|
|
|
const response = await toWebHandler(handler)({ request: createRequest(body, contentType) })
|
|
|
|
expect(handler).toHaveBeenCalledOnce()
|
|
expect(await response.text()).toBe('ok')
|
|
})
|
|
|
|
it.each(['GET', 'HEAD'])('leaves %s bodies undefined', async (method) => {
|
|
const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => {
|
|
expect(req.method).toBe(method)
|
|
expect(req.body).toBeUndefined()
|
|
res.end()
|
|
})
|
|
|
|
await toWebHandler(handler)({
|
|
request: new Request('http://localhost/api/test', {
|
|
method,
|
|
headers: { 'content-type': 'application/json' },
|
|
}),
|
|
})
|
|
|
|
expect(handler).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('preserves the URL, headers and route parameter precedence', async () => {
|
|
const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => {
|
|
expect(req.url).toBe('/api/test?ref=search&tag=one&tag=two&empty=')
|
|
expect(req.headers['x-custom']).toBe('value')
|
|
expect(req.query).toEqual({ ref: 'route', tag: ['one', 'two'], empty: '' })
|
|
res.end('ok')
|
|
})
|
|
|
|
await toWebHandler(handler)({
|
|
request: new Request('http://localhost/api/test?ref=search&tag=one&tag=two&empty=', {
|
|
headers: { 'X-Custom': 'value' },
|
|
}),
|
|
params: { ref: 'route', omitted: undefined },
|
|
})
|
|
|
|
expect(handler).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it.each(['%', '%ZZ', '%E0%A4%A', '%FF'])(
|
|
'preserves undecodable cookie values: %s',
|
|
async (bad) => {
|
|
const handler = vi.fn((req: NextApiRequest, res: NextApiResponse) => {
|
|
expect(req.cookies).toEqual({ bad, good: 'hello world', token: 'a=b', empty: '' })
|
|
res.json({ success: true })
|
|
})
|
|
|
|
const response = await toWebHandler(handler)({
|
|
request: new Request('http://localhost/api/test', {
|
|
headers: { cookie: `bad=${bad}; good=hello%20world; token=a=b; empty=` },
|
|
}),
|
|
})
|
|
|
|
expect(response.status).toBe(200)
|
|
expect(await response.json()).toEqual({ success: true })
|
|
expect(handler).toHaveBeenCalledOnce()
|
|
}
|
|
)
|
|
|
|
it('does not hide a body-read failure as invalid JSON', async () => {
|
|
const request = createRequest('{}')
|
|
const error = new Error('Body read failed')
|
|
vi.spyOn(request, 'text').mockRejectedValue(error)
|
|
const handler = vi.fn()
|
|
|
|
await expect(toWebHandler(handler)({ request })).rejects.toBe(error)
|
|
expect(handler).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('does not turn a handler SyntaxError into a request parsing error', async () => {
|
|
const error = new SyntaxError('Handler failed')
|
|
const handler = vi.fn(() => {
|
|
throw error
|
|
})
|
|
|
|
await expect(toWebHandler(handler)({ request: createRequest('{}') })).rejects.toBe(error)
|
|
expect(handler).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('passes through a Web Response returned by the handler', async () => {
|
|
const expected = new Response('direct', { status: 202, headers: { 'x-direct': 'yes' } })
|
|
|
|
const response = await toWebHandler(() => expected)({ request: createRequest('{}') })
|
|
|
|
expect(response).toBe(expected)
|
|
expect(await response.text()).toBe('direct')
|
|
})
|
|
|
|
it('keeps streamed responses open for chunks written after the handler returns', async () => {
|
|
let finish: (() => void) | undefined
|
|
const response = await toWebHandler((_req, res) => {
|
|
res.writeHead(202, { 'content-type': 'text/event-stream', 'x-stream': 'yes' })
|
|
res.write('first\n')
|
|
finish = () => res.end('last\n')
|
|
})({ request: createRequest('{}') })
|
|
|
|
expect(response.status).toBe(202)
|
|
expect(response.headers.get('x-stream')).toBe('yes')
|
|
const reader = response.body!.getReader()
|
|
const decoder = new TextDecoder()
|
|
expect(decoder.decode((await reader.read()).value)).toBe('first\n')
|
|
finish!()
|
|
expect(decoder.decode((await reader.read()).value)).toBe('last\n')
|
|
expect((await reader.read()).done).toBe(true)
|
|
})
|
|
|
|
it('preserves close and aborted events from the request signal', async () => {
|
|
const controller = new AbortController()
|
|
const onClose = vi.fn()
|
|
const onAborted = vi.fn()
|
|
await toWebHandler((req, res) => {
|
|
req.on('close', onClose)
|
|
req.once('aborted', onAborted)
|
|
res.end('ok')
|
|
})({ request: new Request('http://localhost/api/test', { signal: controller.signal }) })
|
|
|
|
controller.abort()
|
|
|
|
expect(onClose).toHaveBeenCalledOnce()
|
|
expect(onAborted).toHaveBeenCalledOnce()
|
|
})
|
|
})
|