Skip to content

Commit 3010449

Browse files
committed
test(files): validate the produced archive with a real zip reader
Every existing test mocks the storage stream and reads the result back with the same JS library that wrote it, which cannot catch the failure this route exists to fix: an archive a real zip reader will not open. This drives 5 MB of non-repeating bytes from an fs stream through archiver, the lazy entry generator and the Node-to-web bridge, then checks the output with the operating system's unzip and compares the extracted bytes. Skips rather than fails where unzip is unavailable.
1 parent 18bf362 commit 3010449

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* @vitest-environment node
3+
*
4+
* Exercises the real archive plumbing — archiver, the lazy entry generator, and the
5+
* Node-to-web bridge — against multi-chunk file streams, and validates the result with
6+
* the operating system's `unzip` rather than the same JS library that produced it. The
7+
* bug this route exists to fix was an archive a real zip reader could not open, so a
8+
* self-consistent JS round-trip is not the assertion that matters.
9+
*/
10+
import { execFile } from 'node:child_process'
11+
import { createReadStream } from 'node:fs'
12+
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
13+
import { tmpdir } from 'node:os'
14+
import { join } from 'node:path'
15+
import { promisify } from 'node:util'
16+
import { createMockRequest } from '@sim/testing'
17+
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
18+
19+
const run = promisify(execFile)
20+
21+
const {
22+
mockGetSession,
23+
mockVerifyWorkspaceMembership,
24+
mockListWorkspaceFiles,
25+
mockListFolders,
26+
mockDownloadFileStream,
27+
} = vi.hoisted(() => ({
28+
mockGetSession: vi.fn(),
29+
mockVerifyWorkspaceMembership: vi.fn(),
30+
mockListWorkspaceFiles: vi.fn(),
31+
mockListFolders: vi.fn(),
32+
mockDownloadFileStream: vi.fn(),
33+
}))
34+
35+
vi.mock('@/lib/auth', () => ({
36+
auth: { api: { getSession: vi.fn() } },
37+
getSession: mockGetSession,
38+
}))
39+
vi.mock('@/app/api/workflows/utils', () => ({
40+
verifyWorkspaceMembership: mockVerifyWorkspaceMembership,
41+
}))
42+
vi.mock('@/lib/uploads/contexts/workspace', () => ({
43+
listWorkspaceFiles: mockListWorkspaceFiles,
44+
listWorkspaceFileFolders: mockListFolders,
45+
buildWorkspaceFileFolderPathMap: (folders: Array<{ id: string; name: string }>) =>
46+
new Map(folders.map((folder) => [folder.id, folder.name])),
47+
fetchServableWorkspaceFileBuffer: vi.fn(),
48+
}))
49+
vi.mock('@/lib/uploads/core/storage-service', () => ({
50+
downloadFileStream: mockDownloadFileStream,
51+
}))
52+
vi.mock('@sim/audit', () => ({
53+
recordAudit: vi.fn(),
54+
AuditAction: { FILE_DOWNLOADED: 'file.downloaded' },
55+
AuditResourceType: { FILE: 'file' },
56+
}))
57+
vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: vi.fn() }))
58+
59+
import { GET } from '@/app/api/workspaces/[id]/files/download/route'
60+
61+
const WORKSPACE_ID = 'ws-1'
62+
const context = { params: Promise.resolve({ id: WORKSPACE_ID }) }
63+
64+
let workDir: string
65+
let bigPath: string
66+
let bigBytes: Buffer
67+
/** The OS unzip is the point of this file; skip rather than fail where it is absent. */
68+
let hasUnzip = true
69+
70+
beforeAll(async () => {
71+
hasUnzip = await run('unzip', ['-v']).then(
72+
() => true,
73+
() => false
74+
)
75+
workDir = await mkdtemp(join(tmpdir(), 'sim-archive-'))
76+
bigPath = join(workDir, 'big.bin')
77+
// Several MB of non-repeating bytes: forces many 64 KiB chunks through the generator,
78+
// the archiver queue and the web bridge, and would expose a truncation or ordering bug.
79+
bigBytes = Buffer.alloc(5 * 1024 * 1024)
80+
for (let i = 0; i < bigBytes.length; i++) bigBytes[i] = (i * 31 + (i >> 8)) & 0xff
81+
await writeFile(bigPath, bigBytes)
82+
})
83+
84+
afterAll(async () => {
85+
await rm(workDir, { recursive: true, force: true })
86+
})
87+
88+
describe('workspace files download — real archive', () => {
89+
beforeEach(() => {
90+
vi.clearAllMocks()
91+
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
92+
mockVerifyWorkspaceMembership.mockResolvedValue({ role: 'member' })
93+
mockListFolders.mockResolvedValue([{ id: 'folder-1', name: 'Reports', parentId: null }])
94+
// A real fs stream, not a single pre-made buffer.
95+
mockDownloadFileStream.mockImplementation(async () => createReadStream(bigPath))
96+
})
97+
98+
it('produces an archive the OS unzip accepts, with byte-exact contents', async (ctx) => {
99+
if (!hasUnzip) ctx.skip()
100+
mockListWorkspaceFiles.mockResolvedValue([
101+
{
102+
id: 'f1',
103+
name: 'big.bin',
104+
key: `workspace/${WORKSPACE_ID}/f1`,
105+
path: '/serve/f1',
106+
size: bigBytes.length,
107+
type: 'application/octet-stream',
108+
folderId: 'folder-1',
109+
},
110+
])
111+
112+
const response = await GET(
113+
createMockRequest(
114+
'GET',
115+
undefined,
116+
{},
117+
`http://localhost:3000/api/workspaces/${WORKSPACE_ID}/files/download?fileIds=f1`
118+
),
119+
context
120+
)
121+
expect(response.status).toBe(200)
122+
123+
const zipPath = join(workDir, 'out.zip')
124+
await writeFile(zipPath, Buffer.from(await response.arrayBuffer()))
125+
126+
// Independent validation: the CRCs and central directory have to satisfy a real
127+
// zip reader, which is exactly what failed for the customer.
128+
await expect(run('unzip', ['-t', zipPath])).resolves.toBeTruthy()
129+
130+
await run('unzip', ['-o', '-q', zipPath, '-d', join(workDir, 'out')])
131+
const extracted = await readFile(join(workDir, 'out', 'Reports', 'big.bin'))
132+
expect(extracted.length).toBe(bigBytes.length)
133+
expect(extracted.equals(bigBytes)).toBe(true)
134+
})
135+
136+
it('keeps entries intact and correctly named across several streamed files', async (ctx) => {
137+
if (!hasUnzip) ctx.skip()
138+
mockListWorkspaceFiles.mockResolvedValue(
139+
['a.bin', 'b.bin', 'c.bin'].map((name, index) => ({
140+
id: `f${index}`,
141+
name,
142+
key: `workspace/${WORKSPACE_ID}/f${index}`,
143+
path: `/serve/f${index}`,
144+
size: bigBytes.length,
145+
type: 'application/octet-stream',
146+
folderId: 'folder-1',
147+
}))
148+
)
149+
150+
const response = await GET(
151+
createMockRequest(
152+
'GET',
153+
undefined,
154+
{},
155+
`http://localhost:3000/api/workspaces/${WORKSPACE_ID}/files/download?fileIds=f0&fileIds=f1&fileIds=f2`
156+
),
157+
context
158+
)
159+
160+
const zipPath = join(workDir, 'multi.zip')
161+
await writeFile(zipPath, Buffer.from(await response.arrayBuffer()))
162+
await expect(run('unzip', ['-t', zipPath])).resolves.toBeTruthy()
163+
164+
const { stdout } = await run('unzip', ['-l', zipPath])
165+
for (const name of ['Reports/a.bin', 'Reports/b.bin', 'Reports/c.bin']) {
166+
expect(stdout).toContain(name)
167+
}
168+
// Each entry carries the full payload — a shared or truncated stream would not.
169+
expect(stdout.match(/5242880/g)).toHaveLength(3)
170+
})
171+
})

0 commit comments

Comments
 (0)