@@ -53,6 +53,15 @@ vi.mock('@/lib/uploads/providers/blob/client', () => ({
5353
5454vi . mock ( '@/lib/workspaces/permissions/utils' , ( ) => permissionsMock )
5555
56+ const { mockCheckStorageQuota, mockInitiateS3MultipartUpload } = vi . hoisted ( ( ) => ( {
57+ mockCheckStorageQuota : vi . fn ( ) ,
58+ mockInitiateS3MultipartUpload : vi . fn ( ) ,
59+ } ) )
60+
61+ vi . mock ( '@/lib/billing/storage' , ( ) => ( {
62+ checkStorageQuota : mockCheckStorageQuota ,
63+ } ) )
64+
5665import { POST } from '@/app/api/files/multipart/route'
5766
5867const tokenPayload = {
@@ -200,3 +209,69 @@ describe('POST /api/files/multipart action=complete', () => {
200209 expect ( mockCompleteS3MultipartUpload ) . toHaveBeenCalledTimes ( 2 )
201210 } )
202211} )
212+
213+ describe ( 'POST /api/files/multipart action=initiate quota enforcement' , ( ) => {
214+ const makeInitiateRequest = ( body : unknown ) =>
215+ new NextRequest ( 'http://localhost/api/files/multipart?action=initiate' , {
216+ method : 'POST' ,
217+ headers : { 'Content-Type' : 'application/json' } ,
218+ body : JSON . stringify ( body ) ,
219+ } )
220+
221+ beforeEach ( ( ) => {
222+ vi . clearAllMocks ( )
223+ authMockFns . mockGetSession . mockResolvedValue ( { user : { id : 'user-1' } } )
224+ permissionsMockFns . mockGetUserEntityPermissions . mockResolvedValue ( 'write' )
225+ mockIsUsingCloudStorage . mockReturnValue ( true )
226+ mockGetStorageProvider . mockReturnValue ( 's3' )
227+ mockGetStorageConfig . mockReturnValue ( { bucket : 'b' , region : 'r' } )
228+ mockSignUploadToken . mockReturnValue ( 'signed-token' )
229+ mockCheckStorageQuota . mockResolvedValue ( { allowed : true } )
230+ mockInitiateS3MultipartUpload . mockResolvedValue ( { uploadId : 'up-1' , key : 'k/file.bin' } )
231+ } )
232+
233+ it ( 'blocks upload when fileSize: 0 exceeds quota' , async ( ) => {
234+ mockCheckStorageQuota . mockResolvedValue ( { allowed : false , error : 'Storage limit exceeded' } )
235+
236+ const res = await makeInitiateRequest ( {
237+ fileName : 'file.bin' ,
238+ contentType : 'application/octet-stream' ,
239+ fileSize : 0 ,
240+ workspaceId : 'ws-1' ,
241+ context : 'knowledge-base' ,
242+ } )
243+
244+ const response = await POST ( res )
245+ expect ( response . status ) . toBe ( 413 )
246+ const body = await response . json ( )
247+ expect ( body . error ) . toContain ( 'Storage limit exceeded' )
248+ } )
249+
250+ it ( 'does not check quota for quota-exempt contexts (og-images)' , async ( ) => {
251+ const res = await makeInitiateRequest ( {
252+ fileName : 'img.png' ,
253+ contentType : 'image/png' ,
254+ fileSize : 99999 ,
255+ workspaceId : 'ws-1' ,
256+ context : 'og-images' ,
257+ } )
258+
259+ const response = await POST ( res )
260+ expect ( mockCheckStorageQuota ) . not . toHaveBeenCalled ( )
261+ } )
262+
263+ it ( 'rejects logs context — not allowed via the multipart endpoint' , async ( ) => {
264+ const res = await makeInitiateRequest ( {
265+ fileName : 'exec.log' ,
266+ contentType : 'text/plain' ,
267+ fileSize : 1000 ,
268+ workspaceId : 'ws-1' ,
269+ context : 'logs' ,
270+ } )
271+
272+ const response = await POST ( res )
273+ expect ( response . status ) . toBe ( 400 )
274+ const body = await response . json ( )
275+ expect ( body . error ) . toMatch ( / i n v a l i d s t o r a g e c o n t e x t / i)
276+ } )
277+ } )
0 commit comments