22 * @vitest -environment node
33 */
44import type { Logger } from '@sim/logger'
5- import { beforeEach , describe , expect , it , vi } from 'vitest'
5+ import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
66
77const { mockHasCloudStorage, mockResolveFileInputToUrl } = vi . hoisted ( ( ) => ( {
88 mockHasCloudStorage : vi . fn ( ) ,
@@ -18,10 +18,13 @@ vi.mock('@/lib/uploads/utils/file-utils.server', () => ({
1818} ) )
1919
2020import {
21+ createMediaContainer ,
2122 INSTAGRAM_MEDIA_URL_TTL_SECONDS ,
23+ publishMediaContainer ,
24+ resolveIgUserId ,
2225 resolveInstagramCarouselMedia ,
2326 resolveInstagramMedia ,
24- } from '@/app/api/tools/instagram/resolve-media '
27+ } from '@/app/api/tools/instagram/server-utils '
2528
2629const logger = { } as Logger
2730const context = {
@@ -44,40 +47,26 @@ function uploadedFile(overrides: Record<string, unknown> = {}) {
4447beforeEach ( ( ) => {
4548 vi . clearAllMocks ( )
4649 mockHasCloudStorage . mockReturnValue ( true )
47- mockResolveFileInputToUrl . mockImplementation (
48- async ( { file, filePath } : { file ?: { name ?: string } ; filePath ?: string } ) => ( {
49- fileUrl : filePath || `https://signed.example.com/${ file ?. name || 'media' } ` ,
50- } )
51- )
50+ mockResolveFileInputToUrl . mockImplementation ( async ( { file } : { file ?: { name ?: string } } ) => ( {
51+ fileUrl : `https://signed.example.com/${ file ?. name || 'media' } ` ,
52+ } ) )
53+ } )
54+
55+ afterEach ( ( ) => {
56+ vi . unstubAllGlobals ( )
5257} )
5358
5459describe ( 'resolveInstagramMedia' , ( ) => {
55- it ( 'accepts a public HTTPS media URL ' , async ( ) => {
60+ it ( 'rejects non-file inputs before resolving them ' , async ( ) => {
5661 const result = await resolveInstagramMedia ( {
5762 ...context ,
5863 input : 'https://cdn.example.com/photo.jpg' ,
5964 role : 'image' ,
6065 } )
6166
62- expect ( result ) . toEqual ( {
63- media : expect . objectContaining ( {
64- url : 'https://cdn.example.com/photo.jpg' ,
65- kind : 'image' ,
66- mimeType : 'image/jpeg' ,
67- } ) ,
68- } )
69- } )
70-
71- it ( 'rejects a public HTTP media URL before resolving it' , async ( ) => {
72- const result = await resolveInstagramMedia ( {
73- ...context ,
74- input : 'http://cdn.example.com/photo.jpg' ,
75- role : 'image' ,
76- } )
77-
7867 expect ( result . error ) . toEqual ( {
7968 status : 400 ,
80- message : 'Instagram media URLs must use HTTPS so Meta can download them ' ,
69+ message : 'Media must be a Sim file ' ,
8170 } )
8271 expect ( mockResolveFileInputToUrl ) . not . toHaveBeenCalled ( )
8372 } )
@@ -95,22 +84,19 @@ describe('resolveInstagramMedia', () => {
9584 } )
9685 expect ( mockResolveFileInputToUrl ) . toHaveBeenCalledWith ( {
9786 file,
98- filePath : undefined ,
9987 ...context ,
10088 presignExpirySeconds : INSTAGRAM_MEDIA_URL_TTL_SECONDS ,
10189 } )
10290 } )
10391
104- it ( 'requires cloud storage for uploaded files and internal URLs ' , async ( ) => {
92+ it ( 'requires cloud storage for publishing files' , async ( ) => {
10593 mockHasCloudStorage . mockReturnValue ( false )
10694
107- for ( const input of [ uploadedFile ( ) , '/api/files/serve/execution/photo.jpg' ] ) {
108- const result = await resolveInstagramMedia ( { ...context , input, role : 'image' } )
109- expect ( result . error ) . toEqual ( {
110- status : 400 ,
111- message : expect . stringContaining ( 'Cloud storage is required' ) ,
112- } )
113- }
95+ const result = await resolveInstagramMedia ( { ...context , input : uploadedFile ( ) , role : 'image' } )
96+ expect ( result . error ) . toEqual ( {
97+ status : 400 ,
98+ message : expect . stringContaining ( 'Cloud storage is required' ) ,
99+ } )
114100 expect ( mockResolveFileInputToUrl ) . not . toHaveBeenCalled ( )
115101 } )
116102
@@ -164,71 +150,40 @@ describe('resolveInstagramMedia', () => {
164150} )
165151
166152describe ( 'resolveInstagramCarouselMedia' , ( ) => {
167- it ( 'parses JSON input and infers image and video item types sequentially' , async ( ) => {
153+ it ( 'resolves canonical files in order and infers image and video types sequentially' , async ( ) => {
168154 let activeResolutions = 0
169155 let maxActiveResolutions = 0
170- mockResolveFileInputToUrl . mockImplementation ( async ( { filePath } : { filePath ?: string } ) => {
156+ mockResolveFileInputToUrl . mockImplementation ( async ( { file } : { file ?: { name ?: string } } ) => {
171157 activeResolutions += 1
172158 maxActiveResolutions = Math . max ( maxActiveResolutions , activeResolutions )
173159 await Promise . resolve ( )
174160 activeResolutions -= 1
175- return { fileUrl : filePath }
161+ return { fileUrl : `https://signed.example.com/ ${ file ?. name } ` }
176162 } )
177163
178164 const result = await resolveInstagramCarouselMedia (
179- JSON . stringify ( [
180- 'https://cdn.example.com/ carousel-1.jpg',
181- 'https://cdn.example.com/ carousel-2.mp4',
182- ] ) ,
165+ [
166+ uploadedFile ( { name : ' carousel-1.jpg' } ) ,
167+ uploadedFile ( { name : ' carousel-2.mp4', type : 'video/mp4' } ) ,
168+ ] ,
183169 context . userId ,
184170 context . requestId ,
185171 logger
186172 )
187173
188174 expect ( result . items ?. map ( ( { url, kind } ) => ( { url, kind } ) ) ) . toEqual ( [
189- { url : 'https://cdn .example.com/carousel-1.jpg' , kind : 'image' } ,
190- { url : 'https://cdn .example.com/carousel-2.mp4' , kind : 'video' } ,
175+ { url : 'https://signed .example.com/carousel-1.jpg' , kind : 'image' } ,
176+ { url : 'https://signed .example.com/carousel-2.mp4' , kind : 'video' } ,
191177 ] )
192178 expect ( maxActiveResolutions ) . toBe ( 1 )
193179 } )
194180
195- it ( 'supports legacy comma-separated URLs with an explicit video prefix' , async ( ) => {
196- const result = await resolveInstagramCarouselMedia (
197- 'https://cdn.example.com/carousel-1.jpg, video:https://cdn.example.com/carousel-2.mp4' ,
198- context . userId ,
199- context . requestId ,
200- logger
201- )
202-
203- expect ( result . items ?. map ( ( { kind } ) => kind ) ) . toEqual ( [ 'image' , 'video' ] )
204- } )
205-
206- it ( 'infers media types for uploaded file arrays' , async ( ) => {
207- const result = await resolveInstagramCarouselMedia (
208- [
209- uploadedFile ( ) ,
210- uploadedFile ( {
211- id : 'file-2' ,
212- key : 'execution/workflow-1/execution-1/video.mov' ,
213- name : 'video.mov' ,
214- type : 'video/quicktime' ,
215- } ) ,
216- ] ,
217- context . userId ,
218- context . requestId ,
219- logger
220- )
221-
222- expect ( result . items ?. map ( ( { kind } ) => kind ) ) . toEqual ( [ 'image' , 'video' ] )
223- } )
224-
225181 it . each ( [
226182 { count : 1 , label : 'too few' } ,
227183 { count : 11 , label : 'too many' } ,
228184 ] ) ( 'rejects $label carousel items before resolving them' , async ( { count } ) => {
229- const input = Array . from (
230- { length : count } ,
231- ( _ , index ) => `https://cdn.example.com/carousel-${ index + 1 } .jpg`
185+ const input = Array . from ( { length : count } , ( _ , index ) =>
186+ uploadedFile ( { id : `file-${ index + 1 } ` , name : `carousel-${ index + 1 } .jpg` } )
232187 )
233188
234189 const result = await resolveInstagramCarouselMedia (
@@ -245,14 +200,46 @@ describe('resolveInstagramCarouselMedia', () => {
245200 expect ( mockResolveFileInputToUrl ) . not . toHaveBeenCalled ( )
246201 } )
247202
248- it ( 'rejects malformed carousel JSON ' , async ( ) => {
203+ it ( 'rejects non-file string inputs ' , async ( ) => {
249204 const result = await resolveInstagramCarouselMedia (
250- '[not-json ' ,
205+ 'https://example.com/one.jpg,https://example.com/two.jpg ' ,
251206 context . userId ,
252207 context . requestId ,
253208 logger
254209 )
255210
256- expect ( result . error ) . toEqual ( { status : 400 , message : 'Carousel media JSON is invalid' } )
211+ expect ( result . error ) . toEqual ( { status : 400 , message : 'Carousel media is required' } )
212+ } )
213+ } )
214+
215+ describe ( 'Instagram publishing requests' , ( ) => {
216+ it ( 'resolves the connected account when no override is supplied' , async ( ) => {
217+ const fetchMock = vi . fn ( ) . mockResolvedValue ( Response . json ( { user_id : 123 } , { status : 200 } ) )
218+ vi . stubGlobal ( 'fetch' , fetchMock )
219+
220+ await expect ( resolveIgUserId ( 'token' ) ) . resolves . toBe ( '123' )
221+ expect ( fetchMock ) . toHaveBeenCalledOnce ( )
222+ } )
223+
224+ it ( 'creates and publishes form-encoded containers' , async ( ) => {
225+ const fetchMock = vi
226+ . fn ( )
227+ . mockResolvedValueOnce ( Response . json ( { id : 'container-1' } , { status : 200 } ) )
228+ . mockResolvedValueOnce ( Response . json ( { id : 'media-1' } , { status : 200 } ) )
229+ vi . stubGlobal ( 'fetch' , fetchMock )
230+
231+ await expect (
232+ createMediaContainer ( 'token' , 'user-1' , { image_url : 'https://signed.example/image.jpg' } )
233+ ) . resolves . toBe ( 'container-1' )
234+ await expect ( publishMediaContainer ( 'token' , 'user-1' , 'container-1' ) ) . resolves . toBe ( 'media-1' )
235+
236+ expect ( fetchMock . mock . calls [ 0 ] ?. [ 1 ] ) . toMatchObject ( {
237+ method : 'POST' ,
238+ body : 'image_url=https%3A%2F%2Fsigned.example%2Fimage.jpg' ,
239+ } )
240+ expect ( fetchMock . mock . calls [ 1 ] ?. [ 1 ] ) . toMatchObject ( {
241+ method : 'POST' ,
242+ body : 'creation_id=container-1' ,
243+ } )
257244 } )
258245} )
0 commit comments