@@ -92,13 +92,19 @@ describe('Firebase Functions > Call', () => {
9292 Record < string , any > ,
9393 { message : string ; code : number ; long : number }
9494 > ( functions , 'dataTest' ) ;
95- const result = await func ( data ) ;
95+ try {
96+
97+ const result = await func ( data ) ;
98+
99+ expect ( result . data ) . to . deep . equal ( {
100+ message : 'stub response' ,
101+ code : 42 ,
102+ long : 420
103+ } ) ;
104+ } catch ( err ) {
105+ console . error ( err )
106+ }
96107
97- expect ( result . data ) . to . deep . equal ( {
98- message : 'stub response' ,
99- code : 42 ,
100- long : 420
101- } ) ;
102108 } ) ;
103109
104110 it ( 'scalars' , async ( ) => {
@@ -226,3 +232,155 @@ describe('Firebase Functions > Call', () => {
226232 await expectError ( func ( ) , 'deadline-exceeded' , 'deadline-exceeded' ) ;
227233 } ) ;
228234} ) ;
235+
236+ describe ( 'Firebase Functions > Stream' , ( ) => {
237+ let app : FirebaseApp ;
238+ const region = 'us-central1' ;
239+
240+ before ( ( ) => {
241+ const useEmulator = ! ! process . env . FIREBASE_FUNCTIONS_EMULATOR_ORIGIN ;
242+ const projectId = useEmulator
243+ ? 'functions-integration-test'
244+ : TEST_PROJECT . projectId ;
245+ const messagingSenderId = 'messaging-sender-id' ;
246+ app = makeFakeApp ( { projectId, messagingSenderId } ) ;
247+ } ) ;
248+
249+ it ( 'successfully streams data and resolves final result' , async ( ) => {
250+ const functions = createTestService ( app , region ) ;
251+ const mockFetch = sinon . stub ( functions , 'fetchImpl' as any ) ;
252+
253+ const mockResponse = new ReadableStream ( {
254+ start ( controller ) {
255+ controller . enqueue ( new TextEncoder ( ) . encode ( 'data: {"message":"Hello"}\n' ) ) ;
256+ controller . enqueue ( new TextEncoder ( ) . encode ( 'data: {"message":"World"}\n' ) ) ;
257+ controller . enqueue ( new TextEncoder ( ) . encode ( 'data: {"result":"Final Result"}\n' ) ) ;
258+ controller . close ( ) ;
259+ }
260+ } ) ;
261+
262+ mockFetch . resolves ( {
263+ body : mockResponse ,
264+ headers : new Headers ( { 'Content-Type' : 'text/event-stream' } ) ,
265+ status : 200 ,
266+ statusText : 'OK' ,
267+ } as Response ) ;
268+
269+ const func = httpsCallable < Record < string , any > , string , string > ( functions , 'streamTest' ) ;
270+ const streamResult = await func . stream ( { } ) ;
271+
272+ const messages : string [ ] = [ ] ;
273+ for await ( const message of streamResult . stream ) {
274+ messages . push ( message ) ;
275+ }
276+
277+ expect ( messages ) . to . deep . equal ( [ 'Hello' , 'World' ] ) ;
278+ expect ( await streamResult . data ) . to . equal ( 'Final Result' ) ;
279+
280+ mockFetch . restore ( ) ;
281+ } ) ;
282+
283+ it ( 'handles network errors' , async ( ) => {
284+ const functions = createTestService ( app , region ) ;
285+ const mockFetch = sinon . stub ( functions , 'fetchImpl' as any ) ;
286+
287+ mockFetch . rejects ( new Error ( 'Network error' ) ) ;
288+
289+ const func = httpsCallable < Record < string , any > , string , string > ( functions , 'errTest' ) ;
290+ const streamResult = await func . stream ( { } ) ;
291+
292+ let errorThrown = false ;
293+ try {
294+ for await ( const _ of streamResult . stream ) {
295+ // This should not execute
296+ }
297+ } catch ( error : unknown ) {
298+ errorThrown = true ;
299+ expect ( ( error as FunctionsError ) . code ) . to . equal ( `${ FUNCTIONS_TYPE } /internal` ) ;
300+ }
301+
302+ expect ( errorThrown ) . to . be . true ;
303+ expect ( streamResult . data ) . to . be . a ( 'promise' ) ;
304+
305+ mockFetch . restore ( ) ;
306+ } ) ;
307+
308+ it ( 'handles server-side errors' , async ( ) => {
309+ const functions = createTestService ( app , region ) ;
310+ const mockFetch = sinon . stub ( functions , 'fetchImpl' as any ) ;
311+
312+ const mockResponse = new ReadableStream ( {
313+ start ( controller ) {
314+ controller . enqueue ( new TextEncoder ( ) . encode ( 'data: {"error":{"status":"INVALID_ARGUMENT","message":"Invalid input"}}\n' ) ) ;
315+ controller . close ( ) ;
316+ }
317+ } ) ;
318+
319+ mockFetch . resolves ( {
320+ body : mockResponse ,
321+ headers : new Headers ( { 'Content-Type' : 'text/event-stream' } ) ,
322+ status : 200 ,
323+ statusText : 'OK' ,
324+ } as Response ) ;
325+
326+ const func = httpsCallable < Record < string , any > , string , string > ( functions , 'errTest' ) ;
327+ const streamResult = await func . stream ( { } ) ;
328+
329+ let errorThrown = false ;
330+ try {
331+ for await ( const _ of streamResult . stream ) {
332+ // This should not execute
333+ }
334+ } catch ( error ) {
335+ errorThrown = true ;
336+ expect ( ( error as FunctionsError ) . code ) . to . equal ( `${ FUNCTIONS_TYPE } /invalid-argument` ) ;
337+ expect ( ( error as FunctionsError ) . message ) . to . equal ( 'Invalid input' ) ;
338+ }
339+
340+ expect ( errorThrown ) . to . be . true ;
341+ expectError ( streamResult . data , "invalid-argument" , "Invalid input" )
342+
343+ mockFetch . restore ( ) ;
344+ } ) ;
345+
346+ it ( 'includes authentication and app check tokens in request headers' , async ( ) => {
347+ const authMock : FirebaseAuthInternal = {
348+ getToken : async ( ) => ( { accessToken : 'auth-token' } )
349+ } as unknown as FirebaseAuthInternal ;
350+ const authProvider = new Provider < FirebaseAuthInternalName > (
351+ 'auth-internal' ,
352+ new ComponentContainer ( 'test' )
353+ ) ;
354+ authProvider . setComponent (
355+ new Component ( 'auth-internal' , ( ) => authMock , ComponentType . PRIVATE )
356+ ) ;
357+
358+ const functions = createTestService ( app , region , authProvider ) ;
359+ const mockFetch = sinon . stub ( functions , 'fetchImpl' as any ) ;
360+
361+ const mockResponse = new ReadableStream ( {
362+ start ( controller ) {
363+ controller . enqueue ( new TextEncoder ( ) . encode ( 'data: {"result":"Success"}\n' ) ) ;
364+ controller . close ( ) ;
365+ }
366+ } ) ;
367+
368+ mockFetch . resolves ( {
369+ body : mockResponse ,
370+ headers : new Headers ( { 'Content-Type' : 'text/event-stream' } ) ,
371+ status : 200 ,
372+ statusText : 'OK' ,
373+ } as Response ) ;
374+
375+ const func = httpsCallable < Record < string , any > , string , string > ( functions , 'errTest' ) ;
376+ await func . stream ( { } ) ;
377+
378+ expect ( mockFetch . calledOnce ) . to . be . true ;
379+ const [ _ , options ] = mockFetch . firstCall . args ;
380+ expect ( options . headers [ 'Authorization' ] ) . to . equal ( 'Bearer auth-token' ) ;
381+ expect ( options . headers [ 'Content-Type' ] ) . to . equal ( 'application/json' ) ;
382+ expect ( options . headers [ 'Accept' ] ) . to . equal ( 'text/event-stream' ) ;
383+
384+ mockFetch . restore ( ) ;
385+ } ) ;
386+ } ) ;
0 commit comments