@@ -141,6 +141,58 @@ describe("handleSigninTokenExchangeInvoke", () => {
141141 expect ( stored ) . toBeNull ( ) ;
142142 } ) ;
143143
144+ it ( "bounds successful User Token service JSON reads and cancels the stream" , async ( ) => {
145+ const state = { canceled : false , enqueued : 0 } ;
146+ const chunkBytes = 1024 * 1024 ;
147+ const stream = new ReadableStream < Uint8Array > ( {
148+ pull ( controller ) {
149+ if ( state . enqueued >= 64 ) {
150+ controller . close ( ) ;
151+ return ;
152+ }
153+ state . enqueued += 1 ;
154+ controller . enqueue ( new Uint8Array ( chunkBytes ) . fill ( 0x61 ) ) ;
155+ } ,
156+ cancel ( ) {
157+ state . canceled = true ;
158+ } ,
159+ } ) ;
160+ const jsonSpy = vi . spyOn ( Response . prototype , "json" ) . mockImplementation ( async ( ) => {
161+ throw new Error ( "raw response.json() should not be used" ) ;
162+ } ) ;
163+ const fetchImpl : MSTeamsSsoFetch = vi . fn ( async ( ) => {
164+ return new Response ( stream , {
165+ status : 200 ,
166+ headers : { "content-type" : "application/json" } ,
167+ } ) ;
168+ } ) ;
169+ const { sso, tokenStore } = createSsoDeps ( { fetchImpl } ) ;
170+
171+ try {
172+ const result = await handleSigninTokenExchangeInvoke ( {
173+ value : { id : "flow-1" , connectionName : "GraphConnection" , token : "exchangeable-token" } ,
174+ user : { userId : "aad-user-guid" , channelId : "msteams" } ,
175+ deps : sso ,
176+ } ) ;
177+
178+ expect ( result . ok ) . toBe ( false ) ;
179+ if ( ! result . ok ) {
180+ expect ( result . code ) . toBe ( "unexpected_response" ) ;
181+ expect ( result . message ) . toMatch ( / J S O N r e s p o n s e e x c e e d s 1 6 7 7 7 2 1 6 b y t e s / ) ;
182+ }
183+ expect ( jsonSpy ) . not . toHaveBeenCalled ( ) ;
184+ expect ( state . enqueued ) . toBeLessThan ( 32 ) ;
185+ expect ( state . canceled ) . toBe ( true ) ;
186+ const stored = await tokenStore . get ( {
187+ connectionName : "GraphConnection" ,
188+ userId : "aad-user-guid" ,
189+ } ) ;
190+ expect ( stored ) . toBeNull ( ) ;
191+ } finally {
192+ jsonSpy . mockRestore ( ) ;
193+ }
194+ } ) ;
195+
144196 it ( "refuses to exchange without a user id" , async ( ) => {
145197 const { fetchImpl, calls } = createFakeFetch ( [ ] ) ;
146198 const { sso } = createSsoDeps ( { fetchImpl } ) ;
0 commit comments