@@ -13,7 +13,12 @@ import {
1313 createSessionPrincipal,
1414} from '@sim/testing/factories/principal.factory'
1515import { createRouteContext } from '@sim/testing/helpers/http'
16+ import { fileReadReceiptMock } from '@sim/testing/mocks/file-read-receipt.mock'
1617import { fileUtilsMock, fileUtilsMockFns } from '@sim/testing/mocks/file-utils.mock'
18+ import {
19+ fileUtilsServerMock,
20+ fileUtilsServerMockFns,
21+ } from '@sim/testing/mocks/file-utils-server.mock'
1722import {
1823 filesAuthorizationMock,
1924 filesAuthorizationMockFns,
@@ -36,6 +41,8 @@ const {
3641 mockReadFile,
3742 mockAuthenticateWorkspaceFile,
3843 mockReadWorkspaceFileContentByKey,
44+ mockReadWorkspaceFileRecordByKey,
45+ mockAuthorizeWorkspaceFileRecordByKey,
3946 mockResolveServableDocBytes,
4047 mockGetContentType,
4148 mockFindLocalFile,
@@ -57,6 +64,8 @@ const {
5764 mockReadFile: vi.fn(),
5865 mockAuthenticateWorkspaceFile: vi.fn(),
5966 mockReadWorkspaceFileContentByKey: vi.fn(),
67+ mockReadWorkspaceFileRecordByKey: vi.fn(),
68+ mockAuthorizeWorkspaceFileRecordByKey: vi.fn(async () => undefined),
6069 mockResolveServableDocBytes: vi.fn(),
6170 mockGetContentType: vi.fn(),
6271 mockFindLocalFile: vi.fn(),
@@ -86,6 +95,9 @@ vi.mock('@/lib/uploads/core/storage-service', () => storageServiceMock)
8695
8796vi.mock('@/lib/uploads/utils/file', () => fileUtilsMock)
8897
98+ vi.mock('@/lib/uploads/utils/file.server', () => fileUtilsServerMock)
99+ vi.mock('@/lib/workspace-files/read-receipt', () => fileReadReceiptMock)
100+
89101vi.mock('@/lib/uploads/server/metadata', () => uploadsMetadataMock)
90102
91103vi.mock('@/lib/uploads/setup.server', () => ({}))
@@ -106,6 +118,10 @@ vi.mock('@/lib/workspace-files/api', () => ({
106118
107119vi.mock('@/lib/workspace-files/application/read-workspace-file-content-by-key', () => ({
108120 readWorkspaceFileContentByKey: { execute: mockReadWorkspaceFileContentByKey },
121+ readWorkspaceFileRecordByKey: {
122+ execute: mockReadWorkspaceFileRecordByKey,
123+ authorize: mockAuthorizeWorkspaceFileRecordByKey,
124+ },
109125}))
110126
111127vi.mock('@/lib/uploads/documents/compile', () => ({
@@ -161,6 +177,17 @@ describe('File Serve API Route', () => {
161177 },
162178 content: Buffer.from('generated source'),
163179 })
180+ mockReadWorkspaceFileRecordByKey.mockResolvedValue({
181+ file: {
182+ id: 'file-1',
183+ workspaceId: 'test-workspace-id',
184+ name: 'report.pdf',
185+ key: 'workspace/test-workspace-id/report.pdf',
186+ type: 'text/x-pdflibjs',
187+ size: 16,
188+ contentUpdatedAt: new Date('2026-09-06'),
189+ },
190+ })
164191 mockResolveServableDocBytes.mockImplementation(
165192 async ({ rawBuffer, fileName }: { rawBuffer: Buffer; fileName: string }) => ({
166193 buffer: rawBuffer,
@@ -173,24 +200,23 @@ describe('File Serve API Route', () => {
173200 mockReadFile(filePath)
174201 )
175202 mockCreateFileResponse.mockImplementation(
176- (file: { buffer: Buffer; contentType: string; filename: string }) => {
203+ (file: { buffer: Buffer; contentType: string; filename: string; cacheControl?: string }) => {
177204 return new Response(file.buffer, {
178205 status: 200,
179206 headers: {
180207 'Content-Type': file.contentType,
208+ ...(file.cacheControl ? { 'Cache-Control': file.cacheControl } : {}),
181209 'Content-Disposition': `inline; filename="${file.filename}"`,
182210 },
183211 })
184212 }
185213 )
186- // Delegates so the existing assertions on the response payload — including its
187- // Cache-Control — read the same call list whichever helper the route reached for.
188214 mockCreateConditionalFileResponse.mockImplementation((file: unknown) =>
189215 mockCreateFileResponse(file)
190216 )
191- mockFileErrorResponse.mockImplementation((error: Error) => {
217+ mockFileErrorResponse.mockImplementation((error: Error, status = 500 ) => {
192218 return new Response(JSON.stringify({ error: error.name, message: error.message }), {
193- status: error.name === 'FileNotFoundError' ? 404 : 500 ,
219+ status: error.name === 'FileNotFoundError' ? 404 : status ,
194220 headers: { 'Content-Type': 'application/json' },
195221 })
196222 })
@@ -284,9 +310,9 @@ describe('File Serve API Route', () => {
284310 // The real createFileErrorResponse owns the status mapping; mirror it here so the
285311 // route's own error path is what decides, not the mock's default 500.
286312 mockFileErrorResponse.mockImplementation(
287- (error: Error) =>
313+ (error: Error, status = 500 ) =>
288314 new Response(JSON.stringify({ error: error.name }), {
289- status: error.name === 'PayloadSizeLimitError' ? 413 : 500 ,
315+ status: error.name === 'PayloadSizeLimitError' ? 413 : status ,
290316 })
291317 )
292318
@@ -316,7 +342,7 @@ describe('File Serve API Route', () => {
316342 mockResolveStoredFileContext.mockResolvedValue('workspace')
317343 mockParseWorkspaceFileKey.mockReturnValue('test-workspace-id')
318344 mockAuthenticateWorkspaceFile.mockResolvedValue(principal)
319- mockResolveServableDocBytes .mockResolvedValue({
345+ fileUtilsServerMockFns.mockDownloadServableFileFromStorage .mockResolvedValue({
320346 buffer: Buffer.from('compiled'),
321347 contentType: 'application/pdf',
322348 ...(dependsOnReferencedFiles ? { dependsOnReferencedFiles: true } : {}),
@@ -325,19 +351,22 @@ describe('File Serve API Route', () => {
325351 const req = createMockRequest({
326352 url: 'http://localhost:3000/api/files/serve/workspace/test-workspace-id/report.pdf?v=1756684800000',
327353 })
328- await GET(req, createRouteContext({ path: ['workspace', 'test-workspace-id', 'report.pdf'] }))
329- return mockCreateFileResponse.mock.calls.at(-1)?.[0]
354+ const response = await GET(
355+ req,
356+ createRouteContext({ path: ['workspace', 'test-workspace-id', 'report.pdf'] })
357+ )
358+ expect(response.status).toBe(200)
359+ return response
330360 }
331361
332362 it('caches a versioned document immutably when its bytes derive from the stored source alone', async () => {
333- expect(await serveVersionedDoc(false)).toEqual(
334- expect.objectContaining({ cacheControl: 'private, max-age=31536000, immutable' })
335- )
363+ const response = await serveVersionedDoc(false)
364+ expect(response.headers.get('Cache-Control')).toBe('private, max-age=31536000, immutable')
336365 })
337366
338- it('attaches a validator to a revalidated response so the next check can be answered 304 ', async () => {
339- await serveVersionedDoc(true)
340- expect(mockCreateConditionalFileResponse).toHaveBeenCalled( )
367+ it('requires revalidation for a versioned document with referenced inputs ', async () => {
368+ const response = await serveVersionedDoc(true)
369+ expect(response.headers.get('Cache-Control')).toBe('private, no-cache, must-revalidate' )
341370 })
342371 })
343372})
0 commit comments