@@ -6,13 +6,21 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
66
77const mocks = vi . hoisted ( ( ) => ( {
88 admit : vi . fn ( ) ,
9+ download : vi . fn ( ) ,
910 updateContent : vi . fn ( ) ,
1011 authenticateV2ApiKey : vi . fn ( ) ,
1112 checkRateLimitDirect : vi . fn ( ) ,
1213 checkRateLimitDirectOrThrow : vi . fn ( ) ,
1314 getUserEmailsByIds : vi . fn ( ) ,
1415} ) )
1516
17+ vi . mock ( '@/lib/workspace-files/application/download-workspace-file' , ( ) => ( {
18+ downloadWorkspaceFileStream : {
19+ operation : { id : 'files.download' , minimumRole : 'read' , workspaceApiKey : 'allow' } ,
20+ execute : mocks . download ,
21+ } ,
22+ } ) )
23+
1624vi . mock ( '@/lib/workspace-files/orchestration' , ( ) => ( {
1725 MAX_WORKSPACE_FILE_INLINE_BODY_BYTES : 70 * 1024 * 1024 ,
1826} ) )
@@ -48,7 +56,7 @@ vi.mock('@/lib/users/queries', () => ({
4856} ) )
4957
5058import { OrchestrationError } from '@/lib/core/orchestration/types'
51- import { PUT } from '@/app/api/v2/files/[fileId]/content/route'
59+ import { GET , PUT } from '@/app/api/v2/files/[fileId]/content/route'
5260
5361const WORKSPACE_ID = 'workspace-1'
5462const FILE_ID = 'wf_1'
@@ -76,6 +84,15 @@ const record = {
7684 uploadedAt : new Date ( '2024-01-01T00:00:00Z' ) ,
7785 updatedAt : new Date ( '2024-01-03T00:00:00Z' ) ,
7886}
87+ const context = { params : Promise . resolve ( { fileId : FILE_ID } ) }
88+
89+ const callGet = ( ) =>
90+ GET (
91+ new NextRequest (
92+ `http://localhost:3000/api/v2/files/${ FILE_ID } /content?workspaceId=${ WORKSPACE_ID } `
93+ ) ,
94+ context
95+ )
7996
8097const callPut = ( body : unknown , contentLength ?: number ) =>
8198 PUT (
@@ -87,9 +104,55 @@ const callPut = (body: unknown, contentLength?: number) =>
87104 } ,
88105 body : typeof body === 'string' ? body : JSON . stringify ( body ) ,
89106 } ) ,
90- { params : Promise . resolve ( { fileId : FILE_ID } ) }
107+ context
91108 )
92109
110+ describe ( 'GET /api/v2/files/[fileId]/content' , ( ) => {
111+ beforeEach ( ( ) => {
112+ vi . clearAllMocks ( )
113+ mocks . authenticateV2ApiKey . mockResolvedValue ( auth )
114+ mocks . checkRateLimitDirect . mockResolvedValue ( {
115+ allowed : true ,
116+ remaining : 599 ,
117+ resetAt : new Date ( '2024-01-01T01:00:00Z' ) ,
118+ } )
119+ mocks . checkRateLimitDirectOrThrow . mockResolvedValue ( {
120+ allowed : true ,
121+ remaining : 99 ,
122+ resetAt : new Date ( '2024-01-01T01:00:00Z' ) ,
123+ } )
124+ mocks . download . mockResolvedValue ( {
125+ file : record ,
126+ stream : new Blob ( [ 'id,name\n' ] ) . stream ( ) ,
127+ } )
128+ } )
129+
130+ it ( 'streams bytes through the binary adapter' , async ( ) => {
131+ const response = await callGet ( )
132+
133+ expect ( response . status ) . toBe ( 200 )
134+ expect ( response . headers . get ( 'Content-Type' ) ) . toBe ( 'text/csv' )
135+ expect ( response . headers . get ( 'Content-Disposition' ) ) . toContain ( 'data.csv' )
136+ expect ( await response . text ( ) ) . toBe ( 'id,name\n' )
137+ expect ( mocks . download ) . toHaveBeenCalledWith ( {
138+ principal : auth . principal ,
139+ input : { fileId : FILE_ID , assertedWorkspaceId : WORKSPACE_ID } ,
140+ request : expect . anything ( ) ,
141+ } )
142+ } )
143+
144+ it ( 'conceals content authorization failures' , async ( ) => {
145+ mocks . download . mockRejectedValue (
146+ new OrchestrationError ( 'forbidden' , 'Insufficient workspace permissions' )
147+ )
148+
149+ const response = await callGet ( )
150+
151+ expect ( response . status ) . toBe ( 404 )
152+ expect ( ( await response . json ( ) ) . error . code ) . toBe ( 'NOT_FOUND' )
153+ } )
154+ } )
155+
93156describe ( 'PUT /api/v2/files/[fileId]/content' , ( ) => {
94157 beforeEach ( ( ) => {
95158 vi . clearAllMocks ( )
0 commit comments