Files
simstudioai--sim/apps/sim/lib/core/utils/stream-limits.test.ts
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

233 lines
7.1 KiB
TypeScript

/**
* @vitest-environment node
*/
import { Readable } from 'stream'
import { describe, expect, it, vi } from 'vitest'
import {
assertContentLengthWithinLimit,
PayloadSizeLimitError,
readFileToBufferWithLimit,
readFormDataWithLimit,
readNodeStreamToBufferWithLimit,
readResponseJsonWithLimit,
readResponseTextWithLimit,
readResponseToBufferWithLimit,
readStreamToBufferWithLimit,
} from '@/lib/core/utils/stream-limits'
function streamFromChunks(chunks: Uint8Array[]): ReadableStream<Uint8Array> {
let index = 0
return new ReadableStream({
pull(controller) {
if (index >= chunks.length) {
controller.close()
return
}
controller.enqueue(chunks[index])
index += 1
},
})
}
function headers(contentLength?: string): Headers {
const headers = new Headers()
if (contentLength !== undefined) headers.set('content-length', contentLength)
return headers
}
describe('stream limits', () => {
it('reads a stream under the limit', async () => {
const buffer = await readStreamToBufferWithLimit(
streamFromChunks([new TextEncoder().encode('hello'), new TextEncoder().encode(' world')]),
{ maxBytes: 32, label: 'test payload' }
)
expect(buffer.toString('utf-8')).toBe('hello world')
})
it('rejects when content-length is over the limit', () => {
expect(() => assertContentLengthWithinLimit(headers('11'), 10, 'download')).toThrow(
PayloadSizeLimitError
)
})
it('cancels response bodies when content-length preflight rejects', async () => {
const cancelSpy = vi.fn()
const body = new ReadableStream<Uint8Array>({
cancel: cancelSpy,
})
await expect(
readResponseToBufferWithLimit(
{
headers: headers('11'),
body,
},
{ maxBytes: 10, label: 'download' }
)
).rejects.toBeInstanceOf(PayloadSizeLimitError)
expect(cancelSpy).toHaveBeenCalled()
})
it('allows content-length exactly at the limit', () => {
expect(() => assertContentLengthWithinLimit(headers('10'), 10, 'download')).not.toThrow()
})
it('rejects when streamed bytes exceed the limit', async () => {
await expect(
readStreamToBufferWithLimit(streamFromChunks([new Uint8Array(6), new Uint8Array(5)]), {
maxBytes: 10,
label: 'download',
})
).rejects.toMatchObject({
name: 'PayloadSizeLimitError',
maxBytes: 10,
observedBytes: 11,
})
})
it('rejects underreported content-length via streamed byte counting', async () => {
await expect(
readResponseToBufferWithLimit(
{
headers: headers('5'),
body: streamFromChunks([new Uint8Array(6), new Uint8Array(5)]),
},
{ maxBytes: 10, label: 'download' }
)
).rejects.toBeInstanceOf(PayloadSizeLimitError)
})
it('returns an empty buffer for a missing body', async () => {
const buffer = await readResponseToBufferWithLimit(
{ headers: headers('0'), body: null },
{ maxBytes: 10, label: 'empty response' }
)
expect(buffer.length).toBe(0)
})
it('reads text and JSON responses with limits', async () => {
const text = await readResponseTextWithLimit(
{ body: streamFromChunks([new TextEncoder().encode('hello')]) },
{ maxBytes: 10, label: 'text response' }
)
const json = await readResponseJsonWithLimit<{ ok: boolean }>(
{ body: streamFromChunks([new TextEncoder().encode('{"ok":true}')]) },
{ maxBytes: 20, label: 'json response' }
)
expect(text).toBe('hello')
expect(json.ok).toBe(true)
})
it('prefers arrayBuffer over text for binary response fallbacks', async () => {
const bytes = Uint8Array.from([0, 255, 1, 254])
const arrayBuffer = vi.fn(async () => bytes.buffer)
const text = vi.fn(async () => 'corrupted')
const buffer = await readResponseToBufferWithLimit(
{ headers: headers(String(bytes.byteLength)), arrayBuffer, text },
{ maxBytes: 10, label: 'binary response' }
)
expect(buffer).toEqual(Buffer.from(bytes))
expect(arrayBuffer).toHaveBeenCalled()
expect(text).not.toHaveBeenCalled()
})
it('rejects no-body response fallbacks without a trusted content-length', async () => {
await expect(
readResponseToBufferWithLimit(
{
arrayBuffer: vi.fn(async () => new Uint8Array(1024).buffer),
},
{ maxBytes: 10, label: 'unknown response' }
)
).rejects.toBeInstanceOf(PayloadSizeLimitError)
})
it('cancels when the abort signal is already aborted', async () => {
const controller = new AbortController()
controller.abort(new Error('stop'))
const cancelSpy = vi.fn()
const stream = new ReadableStream<Uint8Array>({
pull(controller) {
controller.enqueue(new TextEncoder().encode('content'))
},
cancel: cancelSpy,
})
await expect(
readStreamToBufferWithLimit(stream, {
maxBytes: 100,
label: 'abortable',
signal: controller.signal,
})
).rejects.toThrow('stop')
expect(cancelSpy).toHaveBeenCalled()
})
it('checks file size before materializing a File', async () => {
const file = new File(['hello'], 'small.txt', { type: 'text/plain' })
const buffer = await readFileToBufferWithLimit(file, { maxBytes: 5, label: 'upload file' })
expect(buffer.toString('utf-8')).toBe('hello')
await expect(
readFileToBufferWithLimit(file, { maxBytes: 4, label: 'upload file' })
).rejects.toBeInstanceOf(PayloadSizeLimitError)
})
it('parses multipart form data without requiring content-length', async () => {
const input = new FormData()
input.append('name', 'example')
const request = new Request('http://localhost/upload', {
method: 'POST',
body: input,
})
expect(request.headers.get('content-length')).toBeNull()
const formData = await readFormDataWithLimit(request, {
maxBytes: 1024 * 1024,
label: 'multipart body',
})
expect(formData.get('name')).toBe('example')
})
it('rejects multipart streams without content-length once bytes exceed the limit', async () => {
const request = new Request('http://localhost/upload', {
method: 'POST',
headers: { 'content-type': 'multipart/form-data; boundary=test' },
body: streamFromChunks([new Uint8Array(6), new Uint8Array(5)]),
duplex: 'half',
} as RequestInit)
await expect(
readFormDataWithLimit(request, { maxBytes: 10, label: 'multipart body' })
).rejects.toBeInstanceOf(PayloadSizeLimitError)
})
it('rechecks materialized file bytes after arrayBuffer', async () => {
const file = {
size: 1,
arrayBuffer: vi.fn(async () => new Uint8Array(6).buffer),
} as unknown as File
await expect(
readFileToBufferWithLimit(file, { maxBytes: 5, label: 'upload file' })
).rejects.toBeInstanceOf(PayloadSizeLimitError)
})
it('rejects node streams that exceed the limit', async () => {
await expect(
readNodeStreamToBufferWithLimit(Readable.from([Buffer.alloc(6), Buffer.alloc(5)]), {
maxBytes: 10,
label: 'storage download',
})
).rejects.toBeInstanceOf(PayloadSizeLimitError)
})
})