11import { Readable } from 'stream' ;
22
3- interface MockUploadResult {
4- upload : { Location : string } ;
5- contentType : string ;
6- }
7-
83async function loadUploadResolverModule ( opts : {
94 detectedContentType : string ;
10- uploadResultContentType ?: string ;
115} ) {
126 jest . resetModules ( ) ;
137
14- const mockDetectContentType = jest . fn ( ) . mockResolvedValue ( {
8+ const mockStreamContentType = jest . fn ( ) . mockResolvedValue ( {
159 stream : Readable . from ( [ Buffer . alloc ( 16 ) ] ) ,
1610 magic : { type : opts . detectedContentType , charset : 'binary' } ,
1711 contentType : opts . detectedContentType ,
1812 } ) ;
1913
20- const mockUploadWithContentType = jest . fn ( ) . mockResolvedValue ( {
21- upload : { Location : 'https://cdn.example.com/uploaded-file' } ,
22- contentType : opts . uploadResultContentType ?? opts . detectedContentType ,
23- } as MockUploadResult ) ;
14+ const mockUpload = jest . fn ( ) . mockResolvedValue ( { etag : 'test-etag' } ) ;
15+ const mockPresignGet = jest . fn ( ) . mockResolvedValue ( 'https://cdn.example.com/signed-url' ) ;
2416
25- const mockUpload = jest . fn ( ) . mockResolvedValue ( {
26- upload : { Location : 'https://cdn.example.com/storage-upload' } ,
27- contentType : 'application/octet-stream' ,
28- } as MockUploadResult ) ;
17+ const MockS3StorageProvider = jest . fn ( ) . mockImplementation ( ( ) => ( {
18+ upload : mockUpload ,
19+ presignGet : mockPresignGet ,
20+ } ) ) ;
21+
22+ const mockPoolQuery = jest . fn ( ) . mockResolvedValue ( { rows : [ ] , rowCount : 0 } ) ;
23+ const MockPool = jest . fn ( ) . mockImplementation ( ( ) => ( {
24+ query : mockPoolQuery ,
25+ end : jest . fn ( ) ,
26+ } ) ) ;
2927
3028 jest . doMock ( '@constructive-io/graphql-env' , ( ) => ( {
3129 getEnvOptions : jest . fn ( ( ) => ( {
@@ -40,25 +38,24 @@ async function loadUploadResolverModule(opts: {
4038 } ) ) ,
4139 } ) ) ;
4240
43- jest . doMock ( '@constructive-io/s3-streamer' , ( ) => {
44- const StreamerMock = jest . fn ( ) . mockImplementation ( ( ) => ( {
45- upload : mockUpload ,
46- uploadWithContentType : mockUploadWithContentType ,
47- detectContentType : mockDetectContentType ,
48- } ) ) ;
49- return {
50- __esModule : true ,
51- default : StreamerMock ,
52- } ;
53- } ) ;
41+ jest . doMock ( '@constructive-io/s3-streamer' , ( ) => ( {
42+ __esModule : true ,
43+ S3StorageProvider : MockS3StorageProvider ,
44+ streamContentType : mockStreamContentType ,
45+ } ) ) ;
46+
47+ jest . doMock ( 'pg' , ( ) => ( {
48+ Pool : MockPool ,
49+ } ) ) ;
5450
5551 const mod = await import ( '../src/upload-resolver' ) ;
5652
5753 return {
5854 ...mod ,
59- mockDetectContentType,
60- mockUploadWithContentType,
55+ mockStreamContentType,
6156 mockUpload,
57+ mockPresignGet,
58+ mockPoolQuery,
6259 } ;
6360}
6461
@@ -69,12 +66,21 @@ function makeFakeUpload(filename: string) {
6966 } ;
7067}
7168
69+ function makeFakeContext ( databaseId ?: string , userId ?: string ) {
70+ return {
71+ req : {
72+ api : { databaseId } ,
73+ token : { user_id : userId } ,
74+ } ,
75+ } ;
76+ }
77+
7278describe ( 'uploadResolver MIME validation' , ( ) => {
7379 it ( 'rejects disallowed MIME before uploading to storage' , async ( ) => {
7480 const {
7581 constructiveUploadFieldDefinitions,
76- mockDetectContentType ,
77- mockUploadWithContentType ,
82+ mockStreamContentType ,
83+ mockUpload ,
7884 } = await loadUploadResolverModule ( {
7985 detectedContentType : 'application/pdf' ,
8086 } ) ;
@@ -92,23 +98,23 @@ describe('uploadResolver MIME validation', () => {
9298 imageDef . resolve (
9399 fakeUpload as any ,
94100 { } ,
95- { } ,
101+ makeFakeContext ( '1' ) ,
96102 { uploadPlugin : { tags : { } , type : 'image' } } ,
97103 ) ,
98104 ) . rejects . toThrow ( 'UPLOAD_MIMETYPE' ) ;
99105
100- expect ( mockDetectContentType ) . toHaveBeenCalledTimes ( 1 ) ;
101- expect ( mockUploadWithContentType ) . not . toHaveBeenCalled ( ) ;
106+ expect ( mockStreamContentType ) . toHaveBeenCalledTimes ( 1 ) ;
107+ expect ( mockUpload ) . not . toHaveBeenCalled ( ) ;
102108 } ) ;
103109
104110 it ( 'uploads and returns image metadata when MIME is allowed' , async ( ) => {
105111 const {
106112 constructiveUploadFieldDefinitions,
107- mockDetectContentType,
108- mockUploadWithContentType,
113+ mockStreamContentType,
114+ mockUpload,
115+ mockPresignGet,
109116 } = await loadUploadResolverModule ( {
110117 detectedContentType : 'image/png' ,
111- uploadResultContentType : 'image/png' ,
112118 } ) ;
113119
114120 const imageDef = constructiveUploadFieldDefinitions . find (
@@ -123,21 +129,44 @@ describe('uploadResolver MIME validation', () => {
123129 const result = await imageDef . resolve (
124130 fakeUpload as any ,
125131 { } ,
126- { } ,
132+ makeFakeContext ( '1' , 'user-123' ) ,
127133 { uploadPlugin : { tags : { } , type : 'image' } } ,
128134 ) ;
129135
130- expect ( result ) . toEqual ( {
131- filename : 'photo.png' ,
132- mime : 'image/png' ,
133- url : 'https://cdn.example.com/uploaded-file' ,
134- } ) ;
135- expect ( mockDetectContentType ) . toHaveBeenCalledTimes ( 1 ) ;
136- expect ( mockUploadWithContentType ) . toHaveBeenCalledTimes ( 1 ) ;
137- expect ( mockUploadWithContentType ) . toHaveBeenCalledWith (
136+ expect ( result ) . toEqual (
138137 expect . objectContaining ( {
139- contentType : 'image/png' ,
138+ filename : 'photo.png' ,
139+ mime : 'image/png' ,
140+ url : 'https://cdn.example.com/signed-url' ,
141+ key : expect . stringMatching ( / ^ 1 \/ d e f a u l t \/ [ 0 - 9 a - f - ] + _ o r i g i n $ / ) ,
140142 } ) ,
141143 ) ;
144+ expect ( mockStreamContentType ) . toHaveBeenCalledTimes ( 1 ) ;
145+ expect ( mockUpload ) . toHaveBeenCalledTimes ( 1 ) ;
146+ expect ( mockPresignGet ) . toHaveBeenCalledTimes ( 1 ) ;
147+ } ) ;
148+
149+ it ( 'throws when databaseId is missing' , async ( ) => {
150+ const { constructiveUploadFieldDefinitions } = await loadUploadResolverModule ( {
151+ detectedContentType : 'image/png' ,
152+ } ) ;
153+
154+ const imageDef = constructiveUploadFieldDefinitions . find (
155+ ( def ) => 'name' in def && def . name === 'image' ,
156+ ) ;
157+ if ( ! imageDef ) {
158+ throw new Error ( 'Missing image upload field definition' ) ;
159+ }
160+
161+ const fakeUpload = makeFakeUpload ( 'photo.png' ) ;
162+
163+ await expect (
164+ imageDef . resolve (
165+ fakeUpload as any ,
166+ { } ,
167+ { } , // no databaseId
168+ { uploadPlugin : { tags : { } , type : 'image' } } ,
169+ ) ,
170+ ) . rejects . toThrow ( 'databaseId is required' ) ;
142171 } ) ;
143172} ) ;
0 commit comments