Skip to content

Commit f16e140

Browse files
committed
test(v1): cover the file download's rendered-bytes behavior
The route had no tests, and #5995 changed what it serves: rendered bytes, the resolved content type rather than the record's source MIME, Content-Length from the rendered length, and a retryable 409 while an artifact compiles. One test pins the filename/content-type relationship. A review flagged the download as naming a rendered file with a source extension, but the renderer picks its output format from the file name — getE2BDocFormat and COMPILABLE_FORMATS both key on it — so a .docx renders to a docx and the two cannot disagree. The test makes that argument executable rather than a comment.
1 parent fc2ded7 commit f16e140

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { createMockRequest } from '@sim/testing'
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const {
8+
mockCheckRateLimit,
9+
mockValidateWorkspaceAccess,
10+
mockGetWorkspaceFile,
11+
mockFetchServableWorkspaceFileBuffer,
12+
} = vi.hoisted(() => ({
13+
mockCheckRateLimit: vi.fn(),
14+
mockValidateWorkspaceAccess: vi.fn(),
15+
mockGetWorkspaceFile: vi.fn(),
16+
mockFetchServableWorkspaceFileBuffer: vi.fn(),
17+
}))
18+
19+
vi.mock('@/app/api/v1/middleware', () => ({
20+
checkRateLimit: mockCheckRateLimit,
21+
createRateLimitResponse: () => new Response('rate limited', { status: 429 }),
22+
validateWorkspaceAccess: mockValidateWorkspaceAccess,
23+
}))
24+
vi.mock('@/lib/uploads/contexts/workspace', () => ({
25+
getWorkspaceFile: mockGetWorkspaceFile,
26+
fetchServableWorkspaceFileBuffer: mockFetchServableWorkspaceFileBuffer,
27+
}))
28+
vi.mock('@/lib/workspace-files/orchestration', () => ({
29+
performDeleteWorkspaceFileItems: vi.fn(),
30+
}))
31+
vi.mock('@sim/audit', () => ({
32+
recordAudit: vi.fn(),
33+
AuditAction: { FILE_DOWNLOADED: 'file.downloaded', FILE_DELETED: 'file.deleted' },
34+
AuditResourceType: { FILE: 'file' },
35+
}))
36+
vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn() }))
37+
38+
import { DocCompileUserError } from '@/lib/copilot/tools/server/files/doc-compile'
39+
import { GET } from '@/app/api/v1/files/[fileId]/route'
40+
41+
const WORKSPACE_ID = 'ws-1'
42+
const FILE_ID = 'file-1'
43+
const context = { params: Promise.resolve({ fileId: FILE_ID }) }
44+
45+
const DOCX_MIME = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
46+
47+
function request() {
48+
return createMockRequest(
49+
'GET',
50+
undefined,
51+
{},
52+
`http://localhost:3000/api/v1/files/${FILE_ID}?workspaceId=${WORKSPACE_ID}`
53+
)
54+
}
55+
56+
/** A generated document: the name carries the target extension, the type the source. */
57+
function generatedDocument(name = 'report.docx') {
58+
return {
59+
id: FILE_ID,
60+
workspaceId: WORKSPACE_ID,
61+
name,
62+
key: `workspace/${WORKSPACE_ID}/${FILE_ID}`,
63+
path: `/serve/${FILE_ID}`,
64+
size: 6_242,
65+
type: 'text/x-docxjs',
66+
uploadedBy: 'user-1',
67+
uploadedAt: new Date('2026-01-01'),
68+
updatedAt: new Date('2026-01-01'),
69+
}
70+
}
71+
72+
describe('v1 file download', () => {
73+
beforeEach(() => {
74+
vi.clearAllMocks()
75+
mockCheckRateLimit.mockResolvedValue({ allowed: true, userId: 'user-1' })
76+
mockValidateWorkspaceAccess.mockResolvedValue(null)
77+
mockGetWorkspaceFile.mockResolvedValue(generatedDocument())
78+
mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
79+
buffer: Buffer.from('PKrendered'),
80+
contentType: DOCX_MIME,
81+
})
82+
})
83+
84+
it('serves the rendered bytes and the rendered content type', async () => {
85+
const response = await GET(request(), context)
86+
87+
expect(response.status).toBe(200)
88+
// Not the record's `text/x-docxjs`, which describes the stored source.
89+
expect(response.headers.get('Content-Type')).toBe(DOCX_MIME)
90+
expect(Buffer.from(await response.arrayBuffer()).toString()).toContain('rendered')
91+
})
92+
93+
it('names the download with an extension matching the served content type', async () => {
94+
// The renderer picks its output format from the file name, so the two cannot
95+
// disagree: a `.docx` renders to a docx. This pins that invariant.
96+
const response = await GET(request(), context)
97+
98+
const disposition = response.headers.get('Content-Disposition') ?? ''
99+
expect(disposition).toContain('report.docx')
100+
expect(response.headers.get('Content-Type')).toBe(DOCX_MIME)
101+
})
102+
103+
it('reports Content-Length from the rendered bytes, not the declared source size', async () => {
104+
const rendered = Buffer.alloc(50_000)
105+
mockFetchServableWorkspaceFileBuffer.mockResolvedValue({
106+
buffer: rendered,
107+
contentType: DOCX_MIME,
108+
})
109+
110+
const response = await GET(request(), context)
111+
112+
expect(response.headers.get('Content-Length')).toBe(String(rendered.length))
113+
})
114+
115+
it('returns a retryable 409 while the artifact is still compiling', async () => {
116+
mockFetchServableWorkspaceFileBuffer.mockRejectedValue(
117+
new DocCompileUserError('Document is still being generated')
118+
)
119+
120+
const response = await GET(request(), context)
121+
122+
// A 500 would give the caller no reason to try again.
123+
expect(response.status).toBe(409)
124+
expect((await response.json()).error).toContain('still being generated')
125+
})
126+
127+
it('404s a file that does not exist', async () => {
128+
mockGetWorkspaceFile.mockResolvedValue(null)
129+
130+
const response = await GET(request(), context)
131+
132+
expect(response.status).toBe(404)
133+
expect(mockFetchServableWorkspaceFileBuffer).not.toHaveBeenCalled()
134+
})
135+
})

0 commit comments

Comments
 (0)