Files
elysiajs-elysia/test/validator/body.test.ts
T
2026-01-26 08:41:00 +01:00

1695 lines
35 KiB
TypeScript

import { Elysia, t, ValidationError } from '../../src'
import { describe, expect, it } from 'bun:test'
import { post, upload } from '../utils'
describe('Body Validator', () => {
it('skip body parsing if body is empty but headers is present', async () => {
const app = new Elysia().post('/', ({ body }) => 'ok')
const response = await app.handle(
new Request('http://localhost', {
method: 'POST',
headers: {
'content-type': 'application/json'
}
})
)
expect(response.status).toBe(200)
})
it('validate single', async () => {
const app = new Elysia().post('/', ({ body: { name } }) => name, {
body: t.Object({
name: t.String()
})
})
const res = await app.handle(
post('/', {
name: 'sucrose'
})
)
expect(await res.text()).toBe('sucrose')
expect(res.status).toBe(200)
})
it('validate multiple', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Object({
name: t.String(),
job: t.String(),
trait: t.String()
})
})
const res = await app.handle(
post('/', {
name: 'sucrose',
job: 'alchemist',
trait: 'dog'
})
)
expect(await res.json()).toEqual({
name: 'sucrose',
job: 'alchemist',
trait: 'dog'
})
expect(res.status).toBe(200)
})
it('parse without reference', async () => {
const app = new Elysia().post('/', () => '', {
body: t.Object({
name: t.String(),
job: t.String(),
trait: t.String()
})
})
const res = await app.handle(
post('/', {
name: 'sucrose',
job: 'alchemist',
trait: 'dog'
})
)
expect(res.status).toBe(200)
})
it('validate optional', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Object({
name: t.String(),
job: t.String(),
trait: t.Optional(t.String())
})
})
const res = await app.handle(
post('/', {
name: 'sucrose',
job: 'alchemist'
})
)
expect(await res.json()).toEqual({
name: 'sucrose',
job: 'alchemist'
})
expect(res.status).toBe(200)
})
it('parse single numeric', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Object({
name: t.String(),
job: t.String(),
trait: t.Optional(t.String()),
age: t.Numeric()
})
})
const res = await app.handle(
post('/', {
name: 'sucrose',
job: 'alchemist',
age: '16'
})
)
expect(await res.json()).toEqual({
name: 'sucrose',
job: 'alchemist',
age: 16
})
expect(res.status).toBe(200)
})
it('parse multiple numeric', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Object({
name: t.String(),
job: t.String(),
trait: t.Optional(t.String()),
age: t.Numeric(),
rank: t.Numeric()
})
})
const res = await app.handle(
post('/', {
name: 'sucrose',
job: 'alchemist',
age: '16',
rank: '4'
})
)
expect(await res.json()).toEqual({
name: 'sucrose',
job: 'alchemist',
age: 16,
rank: 4
})
expect(res.status).toBe(200)
})
it('parse single integer', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Object({
name: t.String(),
job: t.String(),
trait: t.Optional(t.String()),
age: t.Integer()
})
})
const res = await app.handle(
post('/', {
name: 'sucrose',
job: 'alchemist',
age: '16'
})
)
expect(await res.json()).toEqual({
name: 'sucrose',
job: 'alchemist',
age: 16
})
expect(res.status).toBe(200)
})
it('parse multiple integers', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Object({
name: t.String(),
job: t.String(),
trait: t.Optional(t.String()),
age: t.Integer(),
rank: t.Integer()
})
})
const res = await app.handle(
post('/', {
name: 'sucrose',
job: 'alchemist',
age: '16',
rank: '4'
})
)
expect(await res.json()).toEqual({
name: 'sucrose',
job: 'alchemist',
age: 16,
rank: 4
})
expect(res.status).toBe(200)
})
it('rejects malformed integer from array object', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Array(
t.Object({
name: t.String(),
job: t.String(),
trait: t.Optional(t.String()),
age: t.Integer(),
rank: t.Integer()
})
)
})
const res = await app.handle(
post('/', [
{
name: 'sucrose',
job: 'alchemist',
age: 16.4,
rank: 4
}
])
)
expect(res.status).toBe(422)
})
it('rejects malformed integer directly in array', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Array(t.Integer())
})
const res = await app.handle(post('/', [1, 2, 3, 4.2]))
expect(res.status).toBe(422)
})
it('validate empty body', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Union([
t.Undefined(),
t.Object({
name: t.String(),
job: t.String(),
trait: t.Optional(t.String())
})
])
})
const res = await app.handle(
new Request('http://localhost/', {
method: 'POST'
})
)
expect(res.status).toBe(200)
expect(await res.text()).toBe('')
})
it('validate empty body with partial', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Union([
t.Undefined(),
t.Object({
name: t.String(),
job: t.String(),
trait: t.Optional(t.String()),
age: t.Numeric(),
rank: t.Numeric()
})
])
})
const res = await app.handle(
new Request('http://localhost/', {
method: 'POST'
})
)
expect(res.status).toBe(200)
expect(await res.text()).toEqual('')
})
it('normalize by default', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Object({
name: t.String()
})
})
const res = await app
.handle(
post('/', {
name: 'sucrose',
job: 'alchemist'
})
)
.then((x) => x.json())
expect(res).toEqual({
name: 'sucrose'
})
})
it('strictly validate if not normalize', async () => {
const app = new Elysia({ normalize: false }).post(
'/',
({ body }) => body,
{
body: t.Object({
name: t.String()
})
}
)
const res = await app.handle(
post('/', {
name: 'sucrose',
job: 'alchemist'
})
)
expect(res.status).toBe(422)
})
it('validate maybe empty body', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.MaybeEmpty(
t.Object({
name: t.String(),
job: t.String(),
trait: t.Optional(t.String())
})
)
})
const res = await app.handle(
new Request('http://localhost/', {
method: 'POST'
})
)
expect(res.status).toBe(200)
expect(await res.text()).toBe('')
})
it('validate record', async () => {
const app = new Elysia().post('/', ({ body: { name } }) => name, {
body: t.Record(t.String(), t.String())
})
const res = await app.handle(
post('/', {
name: 'sucrose'
})
)
expect(await res.text()).toBe('sucrose')
expect(res.status).toBe(200)
})
it('validate record inside object', async () => {
const app = new Elysia().post(
'/',
({ body: { name, friends } }) =>
`${name} ~ ${Object.keys(friends).join(' + ')}`,
{
body: t.Object({
name: t.String(),
friends: t.Record(t.String(), t.String())
})
}
)
const res = await app.handle(
post('/', {
name: 'sucrose',
friends: {
amber: 'wizard',
lisa: 'librarian'
}
})
)
expect(await res.text()).toBe('sucrose ~ amber + lisa')
expect(res.status).toBe(200)
})
it('validate optional primitive', async () => {
const app = new Elysia().post('/', ({ body }) => body ?? 'sucrose', {
body: t.Optional(t.String())
})
const [valid, invalid] = await Promise.all([
app.handle(
new Request('http://localhost/', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: 'sucrose'
})
),
app.handle(
new Request('http://localhost/', {
method: 'POST'
})
)
])
expect(await valid.text()).toBe('sucrose')
expect(valid.status).toBe(200)
expect(await invalid.text()).toBe('sucrose')
expect(invalid.status).toBe(200)
})
it('validate optional object', async () => {
const app = new Elysia().post(
'/',
({ body }) => body?.name ?? 'sucrose',
{
body: t.Optional(
t.Object({
name: t.String()
})
)
}
)
const [valid, invalid] = await Promise.all([
app.handle(
post('/', {
name: 'sucrose'
})
),
app.handle(
new Request('http://localhost/', {
method: 'POST'
})
)
])
expect(await valid.text()).toBe('sucrose')
expect(valid.status).toBe(200)
expect(await invalid.text()).toBe('sucrose')
expect(invalid.status).toBe(200)
})
it('create default object body', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Object({
username: t.String(),
password: t.String(),
email: t.Optional(t.String({ format: 'email' })),
isSuperuser: t.Boolean({ default: false })
})
})
const value = await app
.handle(
post('/', {
username: 'nagisa',
password: 'hifumi_daisuki',
email: 'kirifuji_nagisa@trinity.school'
})
)
.then((x) => x.json())
expect(value).toEqual({
username: 'nagisa',
password: 'hifumi_daisuki',
email: 'kirifuji_nagisa@trinity.school',
isSuperuser: false
})
})
it('create default string body', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.String({ default: 'hifumi_daisuki' })
})
const value = await app.handle(post('/')).then((x) => x.text())
expect(value).toBe('hifumi_daisuki')
})
it('create default boolean body', async () => {
const app = new Elysia().post('/', ({ body }) => typeof body, {
body: t.Boolean({ default: true })
})
const value = await app.handle(post('/')).then((x) => x.text())
expect(value).toBe('boolean')
})
it('create default number body', async () => {
const app = new Elysia().post('/', ({ body }) => typeof body, {
body: t.Number({ default: 1 })
})
const value = await app.handle(post('/')).then((x) => x.text())
expect(value).toBe('number')
})
it('create default numeric body', async () => {
const app = new Elysia().post('/', ({ body }) => typeof body, {
body: t.Numeric({ default: 1 })
})
const value = await app.handle(post('/')).then((x) => x.text())
expect(value).toBe('number')
})
it('coerce number to numeric', async () => {
const app = new Elysia().post('/', ({ body }) => typeof body, {
body: t.Number()
})
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: '1'
})
)
expect(response.status).toBe(200)
})
it("don't coerce number object to numeric", async () => {
const app = new Elysia().post('/', ({ body: { id } }) => typeof id, {
body: t.Object({
id: t.Number()
})
})
const response = await app.handle(
post('/', {
id: '1'
})
)
expect(response.status).toBe(422)
})
it('coerce string to boolean', async () => {
const app = new Elysia().post('/', ({ body }) => typeof body, {
body: t.Boolean()
})
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: 'true'
})
)
expect(response.status).toBe(200)
})
it("don't coerce string object to boolean", async () => {
const app = new Elysia().post('/', ({ body: { id } }) => typeof id, {
body: t.Object({
id: t.Boolean()
})
})
const response = await app.handle(
post('/', {
id: 'true'
})
)
expect(response.status).toBe(422)
})
it('handle optional at root', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Optional(
t.Object({
id: t.Numeric()
})
)
})
const res = await Promise.all([
app.handle(post('/')).then((x) => x.json()),
app
.handle(
post('/', {
id: 1
})
)
.then((x) => x.json())
])
expect(res).toEqual([{}, { id: 1 }])
})
it('parse query body with array', async () => {
const app = new Elysia().post('/', ({ body }) => body)
const res = await app.handle(
new Request('https://e.ly', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `tea_party=nagisa&tea_party=mika&tea_party=seia`
})
)
expect(await res.json()).toEqual({
tea_party: ['nagisa', 'mika', 'seia']
})
expect(res.status).toBe(200)
})
it('validate references', async () => {
const job = t.Object(
{
name: t.String()
},
{ $id: 'job' }
)
const person = t.Object({
name: t.String(),
job: t.Ref(job)
})
const app = new Elysia()
.model({ job, person })
.post('/', ({ body: { name, job } }) => `${name} - ${job.name}`, {
body: person
})
const res = await app.handle(
post('/', {
name: 'sucrose',
job: { name: 'alchemist' }
})
)
expect(await res.text()).toBe('sucrose - alchemist')
expect(res.status).toBe(200)
})
it('handle file upload', async () => {
const app = new Elysia().post(
'/single',
({ body: { file } }) => file.size,
{
body: t.Object({
file: t.File()
})
}
)
const { request, size } = upload('/single', {
file: 'millenium.jpg'
})
const response = await app.handle(request).then((r) => r.text())
expect(+response).toBe(size)
})
it('handle file upload using model reference', async () => {
const app = new Elysia()
.model({
a: t.Object({
message: t.String(),
image: t.Optional(t.Files())
})
})
.post('/', ({ body }) => 'ok', {
body: 'a'
})
const { request } = upload('/', {
message: 'Hello, world!'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(200)
})
it('handle file prefix', async () => {
const app = new Elysia()
.post('/pass1', ({ body: { file } }) => file.size, {
body: t.Object({
file: t.File({
type: 'image/*'
})
})
})
.post('/pass2', ({ body: { file } }) => file.size, {
body: t.Object({
file: t.File({
type: ['application/*', 'image/*']
})
})
})
.post('/fail', ({ body: { file } }) => file.size, {
body: t.Object({
file: t.File({
type: 'application/*'
})
})
})
{
const { request, size } = upload('/pass1', {
file: 'millenium.jpg'
})
const response = await app.handle(request).then((r) => r.text())
expect(+response).toBe(size)
}
{
const { request, size } = upload('/pass2', {
file: 'millenium.jpg'
})
const response = await app.handle(request).then((r) => r.text())
// expect(+response).toBe(size)
}
{
const { request } = upload('/fail', {
file: 'millenium.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
})
it('handle file type', async () => {
const app = new Elysia()
.post('/pass1', ({ body: { file } }) => file.size, {
body: t.Object({
file: t.File({
type: 'image/jpeg'
})
})
})
.post('/pass2', ({ body: { file } }) => file.size, {
body: t.Object({
file: t.File({
type: ['image/png', 'image/jpeg']
})
})
})
.post('/fail', ({ body: { file } }) => file.size, {
body: t.Object({
file: t.File({
type: 'image/png'
})
})
})
{
const { request, size } = upload('/pass1', {
file: 'millenium.jpg'
})
const response = await app.handle(request).then((r) => r.text())
expect(+response).toBe(size)
}
{
const { request, size } = upload('/pass2', {
file: 'millenium.jpg'
})
const response = await app.handle(request).then((r) => r.text())
expect(+response).toBe(size)
}
{
const { request } = upload('/fail', {
file: 'millenium.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
})
it('validate actual file', async () => {
const app = new Elysia().post(
'/upload',
({ body: { file } }) => file.size,
{
body: t.Object({
file: t.File({
type: 'image'
})
})
}
)
{
const { request, size } = upload('/upload', {
file: 'millenium.jpg'
})
const response = await app.handle(request).then((r) => r.text())
expect(+response).toBe(size)
}
{
const { request, size } = upload('/upload', {
file: 'fake.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
})
it('validate actual file with multiple type', async () => {
const app = new Elysia().post(
'/upload',
({ body: { file } }) => file.size,
{
body: t.Object({
file: t.File({
type: ['image/png', 'image/jpeg']
})
})
}
)
{
const { request, size } = upload('/upload', {
file: 'millenium.jpg'
})
const response = await app.handle(request).then((r) => r.text())
expect(+response).toBe(size)
}
{
const { request, size } = upload('/upload', {
file: 'fake.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
{
const { request, size } = upload('/upload', {
file: 'kozeki-ui.webp'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
})
it('validate actual file type union', async () => {
const app = new Elysia().post('/', ({ body }) => 'ok', {
body: t.Union([
t.Object({
hello: t.String(),
file: t.File({
type: 'image'
})
}),
t.Object({
world: t.String(),
image: t.File({
type: 'image'
})
}),
t.Object({
donQuixote: t.String()
})
])
})
// case 1 pass
{
const { request, size } = upload('/', {
hello: 'ok',
file: 'millenium.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(200)
}
// case 1 fail
{
const { request, size } = upload('/', {
hello: 'ok',
file: 'fake.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
// case 2 pass
{
const { request, size } = upload('/', {
world: 'ok',
image: 'millenium.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(200)
}
// case 2 fail
{
const { request, size } = upload('/', {
world: 'ok',
image: 'fake.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
// case 3 fail
{
const { request, size } = upload('/', {
donQuixote: 'Limbus Company!'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(200)
}
})
it('validate actual file type union with multiple file type', async () => {
const app = new Elysia().post('/', ({ body }) => 'ok', {
body: t.Union([
t.Object({
hello: t.String(),
file: t.File({
type: 'image'
})
}),
t.Object({
world: t.String(),
image: t.File({
type: ['image/png', 'image/jpeg']
})
}),
t.Object({
donQuixote: t.String()
})
])
})
// case 1 pass
{
const { request, size } = upload('/', {
hello: 'ok',
file: 'millenium.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(200)
}
// case 1 fail
{
const { request, size } = upload('/', {
hello: 'ok',
file: 'fake.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
// case 2 pass
{
const { request, size } = upload('/', {
world: 'ok',
image: 'millenium.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(200)
}
// case 2 fail by fake image
{
const { request, size } = upload('/', {
world: 'ok',
image: 'fake.jpg'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
// case 2 fail by incorrect image type
{
const { request, size } = upload('/', {
world: 'ok',
image: 'kozeki-ui.webp'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(422)
}
// case 3 fail
{
const { request, size } = upload('/', {
donQuixote: 'Limbus Company!'
})
const status = await app.handle(request).then((r) => r.status)
expect(status).toBe(200)
}
})
it('validate actual files', async () => {
const app = new Elysia().post('/', () => 'ok', {
body: t.Object({
file: t.Files({
type: 'image'
})
})
})
// case 1 fail: contains fake image
{
const body = new FormData()
body.append('file', Bun.file('test/images/fake.jpg'))
body.append('file', Bun.file('test/images/kozeki-ui.webp'))
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body
})
)
expect(response.status).toBe(422)
}
// case 2 pass: all valid images
{
const body = new FormData()
body.append('file', Bun.file('test/images/millenium.jpg'))
body.append('file', Bun.file('test/images/kozeki-ui.webp'))
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body
})
)
expect(response.status).toBe(200)
}
})
it('handle body using Transform with Intersect ', async () => {
const app = new Elysia().post('/test', ({ body }) => body, {
body: t.Intersect([
t.Object({ foo: t.String() }),
t.Object({
field: t
.Transform(t.String())
.Decode((decoded) => ({ decoded }))
.Encode((v) => v.decoded)
})
])
})
const response = await app
.handle(
new Request('http://localhost/test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ field: 'bar', foo: 'test' })
})
)
.then((x) => x.json())
expect(response).toEqual({ field: { decoded: 'bar' }, foo: 'test' })
})
it('right rejects missed field with model', async () => {
const model = new Elysia().model(
'user',
t.Object({
username: t.String(),
age: t.Integer()
})
)
const app = new Elysia().use(model).post('/', ({ body }) => body, {
body: 'user'
})
const res = await app.handle(
post('/', {
name: 'sucrose'
})
)
expect(res.status).toBe(422)
})
it('handle coerce TransformDecodeError', async () => {
let err: Error | undefined
const app = new Elysia()
.post('/', ({ body }) => body, {
body: t.Object({
year: t.Numeric({ minimum: 1900, maximum: 2160 })
}),
error({ code, error }) {
switch (code) {
case 'VALIDATION':
err = error
}
}
})
.listen(0)
await app.handle(
post('/', {
year: '3000'
})
)
expect(err instanceof ValidationError).toBe(true)
})
it('handle nested file upload with dot notation', async () => {
const app = new Elysia().post(
'/',
({ body }) => ({
userName: body.user.name,
fileSize: body.user.avatar.size
}),
{
body: t.Object({
user: t.Object({
name: t.String(),
avatar: t.File()
})
})
}
)
const formData = new FormData()
formData.append('user.name', 'John')
formData.append('user.avatar', Bun.file('test/images/millenium.jpg'))
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
userName: 'John',
fileSize: expect.any(Number)
})
})
it('handle nested files upload with dot notation', async () => {
const app = new Elysia().post(
'/',
({ body }) => ({
productName: body.product.name,
fileSizes: body.product.images.map((f) => f.size)
}),
{
body: t.Object({
product: t.Object({
name: t.String(),
images: t.Files()
})
})
}
)
const formData = new FormData()
formData.append('product.name', 'Chair')
formData.append('product.images', Bun.file('test/images/millenium.jpg'))
formData.append('product.images', Bun.file('test/images/aris-yuzu.jpg'))
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
productName: 'Chair',
fileSizes: expect.arrayContaining([
expect.any(Number),
expect.any(Number)
])
})
})
it('handle deeply nested file upload', async () => {
const app = new Elysia().post(
'/',
({ body }) => ({
bio: body.user.profile.bio,
country: body.user.profile.country,
photoSize: body.user.profile.photo.size
}),
{
body: t.Object({
user: t.Object({
profile: t.Object({
bio: t.String(),
country: t.String(),
photo: t.File()
})
})
})
}
)
const formData = new FormData()
formData.append('user.profile.bio', 'Hello World')
formData.append('user.profile.country', 'France')
formData.append(
'user.profile.photo',
Bun.file('test/images/millenium.jpg')
)
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
bio: 'Hello World',
country: 'France',
photoSize: expect.any(Number)
})
})
it('handle multiple nested files', async () => {
const app = new Elysia().post(
'/',
({ body }) => ({
avatarSize: body.user.avatar.size,
coverSize: body.user.cover.size
}),
{
body: t.Object({
user: t.Object({
avatar: t.File(),
cover: t.File()
})
})
}
)
const formData = new FormData()
formData.append('user.avatar', Bun.file('test/images/millenium.jpg'))
formData.append('user.cover', Bun.file('test/images/kozeki-ui.webp'))
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
avatarSize: expect.any(Number),
coverSize: expect.any(Number)
})
})
it('handle mixed nested and flat fields', async () => {
const app = new Elysia().post(
'/',
({ body }) => ({
flatValue: body.flat,
nestedName: body.user.name,
nestedFileSize: body.user.avatar.size
}),
{
body: t.Object({
flat: t.String(),
user: t.Object({
name: t.String(),
avatar: t.File()
})
})
}
)
const formData = new FormData()
formData.append('flat', 'I am flat')
formData.append('user.name', 'Jane')
formData.append('user.avatar', Bun.file('test/images/millenium.jpg'))
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
flatValue: 'I am flat',
nestedName: 'Jane',
nestedFileSize: expect.any(Number)
})
})
it('handle complex nested array with files', async () => {
const app = new Elysia().post(
'/',
({ body }) => ({
productName: body.name,
createFilesCount: body.images.create.length,
updateCount: body.images.update.length,
images: {
create: body.images.create.map((f) => f.size),
update: body.images.update.map((f) => ({
id: f.id,
altText: f.altText,
imgSize: f.img.size
}))
}
}),
{
body: t.Object({
name: t.String(),
images: t.Object({
create: t.Files(),
update: t.Array(
t.Object({
id: t.String(),
img: t.File(),
altText: t.String()
})
)
})
})
}
)
const formData = new FormData()
formData.append('name', 'Test Product')
formData.append('images.create', Bun.file('test/images/millenium.jpg'))
formData.append('images.create', Bun.file('test/images/kozeki-ui.webp'))
formData.append('images.update[0].id', '123')
formData.append(
'images.update[0].img',
Bun.file('test/images/midori.png')
)
formData.append('images.update[0].altText', 'an image')
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
productName: 'Test Product',
createFilesCount: 2,
updateCount: 1,
images: {
create: [expect.any(Number), expect.any(Number)],
update: [
{
id: '123',
altText: 'an image',
imgSize: expect.any(Number)
}
]
}
})
})
it('handle dot notation for standard schema with array and nested file', async () => {
const { z } = await import('zod')
const app = new Elysia().post(
'/',
({ body }) => ({
updateCount: body.images.update.length,
updates: body.images.update.map((item) => ({
id: item.id,
altText: item.altText,
imgSize: item.img.size
}))
}),
{
body: z.object({
images: z.object({
update: z.array(
z.object({
id: z.string(),
img: z.file(),
altText: z.string()
})
)
})
})
}
)
const formData = new FormData()
formData.append(
'images.update[0]',
JSON.stringify({ id: '123', altText: 'an image' })
)
formData.append(
'images.update[0].img',
Bun.file('test/images/midori.png')
)
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
updateCount: 1,
updates: [
{
id: '123',
altText: 'an image',
imgSize: expect.any(Number)
}
]
})
})
it('handle mix of stringify and dot notation', async () => {
const app = new Elysia().post(
'/',
({ body }) => ({
productName: body.name,
metadata: body.metadata,
createFilesCount: body.images.create.length,
updateCount: body.images.update.length,
images: {
create: body.images.create.map((f) => f.size),
update: body.images.update.map((f) => ({
id: f.id,
altText: f.altText,
imgSize: f.img.size
}))
}
}),
{
body: t.Object({
name: t.String(),
metadata: t.Object({
description: t.String(),
price: t.Number(),
inStock: t.Boolean(),
tags: t.Array(t.String()),
category: t.String()
}),
images: t.Object({
create: t.Files(),
update: t.Array(
t.Object({
id: t.String(),
img: t.File(),
altText: t.String()
})
)
})
})
}
)
const formData = new FormData()
formData.append('name', 'Test Product')
formData.append(
'metadata',
JSON.stringify({
description: 'A high-quality product',
price: 29.99,
inStock: true,
tags: ['electronics', 'featured', 'sale'],
category: 'gadgets'
})
)
formData.append('images.create', Bun.file('test/images/millenium.jpg'))
formData.append('images.create', Bun.file('test/images/kozeki-ui.webp'))
formData.append(
'images.update[0]',
JSON.stringify({ id: '123', altText: 'an image' })
)
formData.append(
'images.update[0].img',
Bun.file('test/images/midori.png')
)
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
productName: 'Test Product',
createFilesCount: 2,
updateCount: 1,
metadata: {
description: 'A high-quality product',
price: 29.99,
inStock: true,
tags: ['electronics', 'featured', 'sale'],
category: 'gadgets'
},
images: {
create: [expect.any(Number), expect.any(Number)],
update: [
{
id: '123',
altText: 'an image',
imgSize: expect.any(Number)
}
]
}
})
})
it('should parse sub-array correctly', async () => {
const app = new Elysia().post('/', ({ body }) => body, {
body: t.Object({
imagesOps: t.Object({
options: t.Array(
t.Object({
id: t.String(),
value: t.String()
})
)
})
})
})
const formData = new FormData()
formData.append(
'imagesOps.options',
JSON.stringify([{ id: 'test-id', value: 'test-value' }])
)
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
expect(response.status).toBe(200)
const result = (await response.json()) as any
expect(result.imagesOps.options).toEqual([
{ id: 'test-id', value: 'test-value' }
])
})
it('prevent prototype pollution with __proto__ in nested multipart', async () => {
const app = new Elysia().post('/', ({ body }) => body)
const formData = new FormData()
formData.append('user.name', 'John')
formData.append('__proto__.isAdmin', 'true')
formData.append('user.__proto__.isAdmin', 'true')
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
user: { name: 'John' }
})
// Check that Object.prototype wasn't polluted
const testObj = {}
expect('isAdmin' in testObj).toBe(false)
expect('isAdmin' in {}).toBe(false)
})
it('prevent prototype pollution with constructor in nested multipart', async () => {
const app = new Elysia().post('/', ({ body }) => body)
const formData = new FormData()
formData.append('user.name', 'John')
formData.append('constructor.prototype.isAdmin', 'true')
formData.append('user.constructor', 'bad')
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
user: { name: 'John' }
})
// Check that Object.prototype wasn't polluted
expect('isAdmin' in {}).toBe(false)
})
it('prevent prototype pollution in array notation', async () => {
const app = new Elysia().post('/', ({ body }) => body)
const formData = new FormData()
formData.append('items[0].name', 'Item 1')
formData.append('items[__proto__].isAdmin', 'true')
formData.append('__proto__[0]', 'bad')
const response = await app.handle(
new Request('http://localhost/', {
method: 'POST',
body: formData
})
)
const result = await response.json()
expect(response.status).toBe(200)
expect(result).toMatchObject({
items: [{ name: 'Item 1' }]
})
// Check that Object.prototype wasn't polluted
const testObj = {}
expect('isAdmin' in testObj).toBe(false)
})
})