11import { Environment } from '@internxt/inxt-js' ;
2- import { UploadOptions } from '@internxt/inxt-js/build/lib/core' ;
32import { Readable } from 'stream' ;
43import { deepMocked } from '../../../../../tests/vitest/utils.helper' ;
54import { uploadContentToEnvironment } from './upload-content-to-environment' ;
@@ -13,38 +12,39 @@ vi.mock('../../../../infra/local-file-system/safe-access', () => ({
1312} ) ) ;
1413
1514describe ( 'upload-content-to-environment' , ( ) => {
15+ type UploadOptions = {
16+ source : NodeJS . ReadableStream ;
17+ fileSize : number ;
18+ progressCallback : ( progress : number ) => void ;
19+ abortSignal ?: AbortSignal ;
20+ } ;
21+
1622 const createReadStreamMock = deepMocked ( fs . createReadStream ) ;
1723 const safeAccessMock = vi . mocked ( safeAccessModule . safeAccess ) ;
1824
1925 const SMALL_SIZE = 1024 ;
20- const LARGE_SIZE = 200 * 1024 * 1024 ; // > 100MB threshold
2126
2227 let environment : Environment ;
2328 let abortController : AbortController ;
2429 let fakeStream : Readable ;
25- let capturedOpts : UploadOptions ;
26-
27- function makeActionState ( ) {
28- return { stop : vi . fn ( ) } ;
29- }
30-
31- function makeUploadFn ( actionState = makeActionState ( ) ) {
32- return vi . fn ( ( _bucket : string , opts : UploadOptions ) => {
33- capturedOpts = opts ;
34- return actionState ;
35- } ) ;
36- }
30+ let capturedOpts : UploadOptions | null ;
31+ let uploadMock : ReturnType < typeof vi . fn < ( bucket : string , opts : UploadOptions ) => Promise < string > > > ;
3732
3833 beforeEach ( ( ) => {
3934 abortController = new AbortController ( ) ;
40- capturedOpts = undefined as unknown as UploadOptions ;
35+ capturedOpts = null ;
4136 fakeStream = Object . assign ( new Readable ( { read ( ) { } } ) , { close : vi . fn ( ) , destroy : vi . fn ( ) } ) ;
4237 createReadStreamMock . mockReturnValue ( fakeStream as ReturnType < typeof fs . createReadStream > ) ;
4338 safeAccessMock . mockResolvedValue ( { data : undefined } ) ;
4439
40+ uploadMock = vi . fn ( async ( _bucket : string , opts : UploadOptions ) => {
41+ capturedOpts = opts ;
42+ return 'default-contents-id' ;
43+ } ) ;
44+
4545 environment = {
46- upload : makeUploadFn ( ) ,
47- uploadMultipartFile : makeUploadFn ( ) ,
46+ upload : uploadMock ,
47+ uploadMultipartFile : vi . fn ( ) ,
4848 } as unknown as Environment ;
4949 } ) ;
5050
@@ -61,102 +61,81 @@ describe('upload-content-to-environment', () => {
6161 async function startUpload ( size = SMALL_SIZE ) {
6262 const promise = callUpload ( size ) ;
6363 await Promise . resolve ( ) ;
64- await Promise . resolve ( ) ;
65- await Promise . resolve ( ) ;
6664 return { promise } ;
6765 }
6866
69- function triggerFinished ( err : Error | null , contentsId : string | null ) {
70- capturedOpts . finishedCallback ( err , contentsId ) ;
71- }
72-
7367 it ( 'should resolve with contentsId on successful upload' , async ( ) => {
7468 const contentsId = 'abc123' ;
75- const { promise } = await startUpload ( ) ;
76- expect ( capturedOpts ) . toBeDefined ( ) ;
77- triggerFinished ( null , contentsId ) ;
69+ uploadMock . mockResolvedValue ( contentsId ) ;
7870
79- const result = await promise ;
71+ const result = await callUpload ( ) ;
8072
8173 expect ( result . data ) . toBe ( contentsId ) ;
8274 expect ( result . error ) . toBeUndefined ( ) ;
8375 } ) ;
8476
85- it ( 'should use upload for files below multipart threshold' , async ( ) => {
86- const { promise } = await startUpload ( SMALL_SIZE ) ;
87- triggerFinished ( null , 'id' ) ;
88-
89- await promise ;
90-
91- expect ( environment . upload ) . toHaveBeenCalled ( ) ;
92- expect ( environment . uploadMultipartFile ) . not . toHaveBeenCalled ( ) ;
93- } ) ;
94-
95- it ( 'should use uploadMultipartFile for files above multipart threshold' , async ( ) => {
96- const { promise } = await startUpload ( LARGE_SIZE ) ;
97- triggerFinished ( null , 'id' ) ;
77+ it ( 'should call upload with file size and abortSignal' , async ( ) => {
78+ uploadMock . mockImplementation ( async ( _bucket : string , opts : UploadOptions ) => {
79+ capturedOpts = opts ;
80+ return 'id' ;
81+ } ) ;
9882
99- await promise ;
83+ await callUpload ( SMALL_SIZE ) ;
10084
101- expect ( environment . uploadMultipartFile ) . toHaveBeenCalled ( ) ;
102- expect ( environment . upload ) . not . toHaveBeenCalled ( ) ;
85+ expect ( capturedOpts ?. fileSize ) . toBe ( SMALL_SIZE ) ;
86+ expect ( capturedOpts ?. abortSignal ) . toBe ( abortController . signal ) ;
87+ expect ( uploadMock ) . toHaveBeenCalled ( ) ;
10388 } ) ;
10489
10590 it ( 'should return NOT_ENOUGH_SPACE error when upload fails with "Max space used"' , async ( ) => {
106- const { promise } = await startUpload ( ) ;
107- triggerFinished ( new Error ( 'Max space used' ) , null ) ;
91+ uploadMock . mockRejectedValue ( new Error ( 'Max space used' ) ) ;
10892
109- const result = await promise ;
93+ const result = await callUpload ( ) ;
11094
11195 expect ( result . error ?. cause ) . toBe ( 'NOT_ENOUGH_SPACE' ) ;
11296 } ) ;
11397
11498 it ( 'should return RATE_LIMITED error on 429 with retry_after from message' , async ( ) => {
115- const { promise } = await startUpload ( ) ;
11699 const err = Object . assign ( new Error ( JSON . stringify ( { retry_after : 10 } ) ) , { status : 429 } ) ;
117- triggerFinished ( err , null ) ;
100+ uploadMock . mockRejectedValue ( err ) ;
118101
119- const result = await promise ;
102+ const result = await callUpload ( ) ;
120103
121104 expect ( result . error ?. cause ) . toBe ( 'RATE_LIMITED' ) ;
122105 expect ( result . error ?. message ) . toBe ( '10000' ) ;
123106 } ) ;
124107
125108 it ( 'should return RATE_LIMITED with default delay when retry_after is missing' , async ( ) => {
126- const { promise } = await startUpload ( ) ;
127109 const err = Object . assign ( new Error ( '{}' ) , { status : 429 } ) ;
128- triggerFinished ( err , null ) ;
110+ uploadMock . mockRejectedValue ( err ) ;
129111
130- const result = await promise ;
112+ const result = await callUpload ( ) ;
131113
132114 expect ( result . error ?. cause ) . toBe ( 'RATE_LIMITED' ) ;
133115 expect ( result . error ?. message ) . toBe ( '30000' ) ;
134116 } ) ;
135117
136118 it ( 'should return INTERNAL_SERVER_ERROR on 500+ errors' , async ( ) => {
137- const { promise } = await startUpload ( ) ;
138119 const err = Object . assign ( new Error ( 'Server error' ) , { status : 500 } ) ;
139- triggerFinished ( err , null ) ;
120+ uploadMock . mockRejectedValue ( err ) ;
140121
141- const result = await promise ;
122+ const result = await callUpload ( ) ;
142123
143124 expect ( result . error ?. cause ) . toBe ( 'INTERNAL_SERVER_ERROR' ) ;
144125 } ) ;
145126
146127 it ( 'should return UNKNOWN error for generic errors' , async ( ) => {
147- const { promise } = await startUpload ( ) ;
148- triggerFinished ( new Error ( 'Something went wrong' ) , null ) ;
128+ uploadMock . mockRejectedValue ( new Error ( 'Something went wrong' ) ) ;
149129
150- const result = await promise ;
130+ const result = await callUpload ( ) ;
151131
152132 expect ( result . error ?. cause ) . toBe ( 'UNKNOWN' ) ;
153133 } ) ;
154134
155- it ( 'should return UNKNOWN error when contentsId is null on success' , async ( ) => {
156- const { promise } = await startUpload ( ) ;
157- triggerFinished ( null , null ) ;
135+ it ( 'should return UNKNOWN error when contentsId is empty on success' , async ( ) => {
136+ uploadMock . mockResolvedValue ( '' ) ;
158137
159- const result = await promise ;
138+ const result = await callUpload ( ) ;
160139
161140 expect ( result . error ?. cause ) . toBe ( 'UNKNOWN' ) ;
162141 } ) ;
@@ -179,8 +158,7 @@ describe('upload-content-to-environment', () => {
179158
180159 expect ( result . error ) . toBe ( accessError ) ;
181160 expect ( createReadStreamMock ) . not . toHaveBeenCalled ( ) ;
182- expect ( environment . upload ) . not . toHaveBeenCalled ( ) ;
183- expect ( environment . uploadMultipartFile ) . not . toHaveBeenCalled ( ) ;
161+ expect ( uploadMock ) . not . toHaveBeenCalled ( ) ;
184162 } ) ;
185163
186164 it ( 'should return ACTION_NOT_PERMITTED when createReadStream throws EACCES' , async ( ) => {
@@ -193,35 +171,49 @@ describe('upload-content-to-environment', () => {
193171 expect ( result . error ?. cause ) . toBe ( 'ACTION_NOT_PERMITTED' ) ;
194172 expect ( safeAccessMock ) . toHaveBeenCalledWith ( { absolutePath : '/some/file.txt' } ) ;
195173 expect ( createReadStreamMock ) . toHaveBeenCalledWith ( '/some/file.txt' ) ;
196- expect ( environment . upload ) . not . toHaveBeenCalled ( ) ;
197- expect ( environment . uploadMultipartFile ) . not . toHaveBeenCalled ( ) ;
174+ expect ( uploadMock ) . not . toHaveBeenCalled ( ) ;
198175 expect ( result . data ) . toBeUndefined ( ) ;
199176 } ) ;
200177
201178 it ( 'should return ACTION_NOT_PERMITTED when the read stream emits EACCES' , async ( ) => {
179+ uploadMock . mockImplementation (
180+ async ( _bucket : string , opts : UploadOptions ) =>
181+ await new Promise < string > ( ( _resolve , reject ) => {
182+ opts . source . once ( 'error' , reject ) ;
183+ } ) ,
184+ ) ;
185+
202186 const { promise } = await startUpload ( ) ;
203187
204188 fakeStream . emit ( 'error' , Object . assign ( new Error ( 'permission denied' ) , { code : 'EACCES' } ) ) ;
205- triggerFinished ( null , 'contents-id-after-stream-error' ) ;
206189
207190 const result = await promise ;
208191
209192 expect ( result . error ?. cause ) . toBe ( 'ACTION_NOT_PERMITTED' ) ;
210193 expect ( safeAccessMock ) . toHaveBeenCalledWith ( { absolutePath : '/some/file.txt' } ) ;
211194 expect ( createReadStreamMock ) . toHaveBeenCalledWith ( '/some/file.txt' ) ;
212- expect ( environment . upload ) . toHaveBeenCalledTimes ( 1 ) ;
213- expect ( environment . uploadMultipartFile ) . not . toHaveBeenCalled ( ) ;
195+ expect ( uploadMock ) . toHaveBeenCalledTimes ( 1 ) ;
214196 expect ( result . data ) . toBeUndefined ( ) ;
215197 } ) ;
216198
217- it ( 'should stop the upload and destroy the stream when signal is aborted' , async ( ) => {
218- const actionState = makeActionState ( ) ;
219- ( environment . upload as unknown as ReturnType < typeof makeUploadFn > ) = makeUploadFn ( actionState ) ;
199+ it ( 'should abort the upload and destroy the stream when signal is aborted' , async ( ) => {
200+ uploadMock . mockImplementation (
201+ async ( _bucket : string , opts : UploadOptions ) =>
202+ await new Promise < string > ( ( _resolve , reject ) => {
203+ opts . abortSignal ?. addEventListener (
204+ 'abort' ,
205+ ( ) => {
206+ reject ( new Error ( 'aborted' ) ) ;
207+ } ,
208+ { once : true } ,
209+ ) ;
210+ } ) ,
211+ ) ;
220212
221- await startUpload ( ) ;
213+ const { promise } = await startUpload ( ) ;
222214 abortController . abort ( ) ;
215+ await promise ;
223216
224- expect ( actionState . stop ) . toHaveBeenCalled ( ) ;
225217 expect ( fakeStream . destroy ) . toHaveBeenCalled ( ) ;
226218 } ) ;
227219} ) ;
0 commit comments