@@ -158,6 +158,203 @@ describe("AccountsPage", () => {
158158 ) . toBeInTheDocument ( ) ;
159159 } ) ;
160160
161+ it ( "does not open browser when oauth authorize response misses device code" , async ( ) => {
162+ const fetchMock = vi . fn ( ( input : RequestInfo | URL , init ?: RequestInit ) => {
163+ const url = String ( input ) ;
164+ if (
165+ url === "/ai-router/api/accounts" &&
166+ ( ! init ?. method || init . method === "GET" )
167+ ) {
168+ return Promise . resolve (
169+ new Response ( JSON . stringify ( [ ] ) , {
170+ status : 200 ,
171+ headers : { "Content-Type" : "application/json" } ,
172+ } ) ,
173+ ) ;
174+ }
175+ if (
176+ url === "/ai-router/api/accounts/usage" &&
177+ ( ! init ?. method || init . method === "GET" )
178+ ) {
179+ return Promise . resolve (
180+ new Response ( JSON . stringify ( [ ] ) , {
181+ status : 200 ,
182+ headers : { "Content-Type" : "application/json" } ,
183+ } ) ,
184+ ) ;
185+ }
186+ if (
187+ url === "/ai-router/api/accounts/auth/authorize" &&
188+ init ?. method === "POST"
189+ ) {
190+ return Promise . resolve (
191+ new Response (
192+ JSON . stringify ( {
193+ authorization_url : "https://auth.openai.com/codex/device" ,
194+ state : "state-1" ,
195+ } ) ,
196+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
197+ ) ,
198+ ) ;
199+ }
200+ return Promise . resolve ( new Response ( null , { status : 404 } ) ) ;
201+ } ) ;
202+ vi . stubGlobal ( "fetch" , fetchMock ) ;
203+
204+ renderAccountsPage ( ) ;
205+
206+ fireEvent . click ( await screen . findByRole ( "button" , { name : / 添 加 账 户 / } ) ) ;
207+ fireEvent . click ( await screen . findByText ( "官方账户" ) ) ;
208+ const officialModal = await screen . findByRole ( "dialog" , {
209+ name : "添加官方账户" ,
210+ } ) ;
211+ fireEvent . click (
212+ within ( officialModal ) . getByRole ( "button" , { name : "使用 ChatGPT 登录" } ) ,
213+ ) ;
214+
215+ await waitFor ( ( ) => {
216+ expect ( fetchMock ) . toHaveBeenCalledWith (
217+ "/ai-router/api/accounts/auth/authorize" ,
218+ expect . objectContaining ( { method : "POST" } ) ,
219+ ) ;
220+ expect ( openExternalUrl ) . not . toHaveBeenCalled ( ) ;
221+ expect ( within ( officialModal ) . queryByText ( "设备码" ) ) . toBeNull ( ) ;
222+ expect (
223+ within ( officialModal ) . getByRole ( "button" , { name : "使用 ChatGPT 登录" } ) ,
224+ ) . toBeInTheDocument ( ) ;
225+ } ) ;
226+ } ) ;
227+
228+ it ( "retries oauth authorize up to 3 times and succeeds on the third try" , async ( ) => {
229+ let authorizeCalls = 0 ;
230+ const fetchMock = vi . fn ( ( input : RequestInfo | URL , init ?: RequestInit ) => {
231+ const url = String ( input ) ;
232+ if (
233+ url === "/ai-router/api/accounts" &&
234+ ( ! init ?. method || init . method === "GET" )
235+ ) {
236+ return Promise . resolve (
237+ new Response ( JSON . stringify ( [ ] ) , {
238+ status : 200 ,
239+ headers : { "Content-Type" : "application/json" } ,
240+ } ) ,
241+ ) ;
242+ }
243+ if (
244+ url === "/ai-router/api/accounts/usage" &&
245+ ( ! init ?. method || init . method === "GET" )
246+ ) {
247+ return Promise . resolve (
248+ new Response ( JSON . stringify ( [ ] ) , {
249+ status : 200 ,
250+ headers : { "Content-Type" : "application/json" } ,
251+ } ) ,
252+ ) ;
253+ }
254+ if (
255+ url === "/ai-router/api/accounts/auth/authorize" &&
256+ init ?. method === "POST"
257+ ) {
258+ authorizeCalls += 1 ;
259+ if ( authorizeCalls < 3 ) {
260+ return Promise . resolve ( new Response ( "temporary unavailable" , { status : 502 } ) ) ;
261+ }
262+ return Promise . resolve (
263+ new Response (
264+ JSON . stringify ( {
265+ authorization_url : "https://auth.openai.com/codex/device" ,
266+ state : "state-3" ,
267+ user_code : "IJKL-MNOP" ,
268+ device_code : "device-auth-id-3" ,
269+ verification_uri : "https://auth.openai.com/codex/device" ,
270+ } ) ,
271+ { status : 200 , headers : { "Content-Type" : "application/json" } } ,
272+ ) ,
273+ ) ;
274+ }
275+ return Promise . resolve ( new Response ( null , { status : 404 } ) ) ;
276+ } ) ;
277+ vi . stubGlobal ( "fetch" , fetchMock ) ;
278+
279+ renderAccountsPage ( ) ;
280+
281+ fireEvent . click ( await screen . findByRole ( "button" , { name : / 添 加 账 户 / } ) ) ;
282+ fireEvent . click ( await screen . findByText ( "官方账户" ) ) ;
283+ const officialModal = await screen . findByRole ( "dialog" , {
284+ name : "添加官方账户" ,
285+ } ) ;
286+ fireEvent . click (
287+ within ( officialModal ) . getByRole ( "button" , { name : "使用 ChatGPT 登录" } ) ,
288+ ) ;
289+
290+ await waitFor ( ( ) => {
291+ expect ( authorizeCalls ) . toBe ( 3 ) ;
292+ expect ( openExternalUrl ) . toHaveBeenCalledWith (
293+ "https://auth.openai.com/codex/device" ,
294+ ) ;
295+ expect ( within ( officialModal ) . getByText ( "设备码" ) ) . toBeInTheDocument ( ) ;
296+ expect ( within ( officialModal ) . getByText ( "IJKL-MNOP" ) ) . toBeInTheDocument ( ) ;
297+ } ) ;
298+ } ) ;
299+
300+ it ( "stops after 3 oauth authorize retries and does not open browser" , async ( ) => {
301+ let authorizeCalls = 0 ;
302+ const fetchMock = vi . fn ( ( input : RequestInfo | URL , init ?: RequestInit ) => {
303+ const url = String ( input ) ;
304+ if (
305+ url === "/ai-router/api/accounts" &&
306+ ( ! init ?. method || init . method === "GET" )
307+ ) {
308+ return Promise . resolve (
309+ new Response ( JSON . stringify ( [ ] ) , {
310+ status : 200 ,
311+ headers : { "Content-Type" : "application/json" } ,
312+ } ) ,
313+ ) ;
314+ }
315+ if (
316+ url === "/ai-router/api/accounts/usage" &&
317+ ( ! init ?. method || init . method === "GET" )
318+ ) {
319+ return Promise . resolve (
320+ new Response ( JSON . stringify ( [ ] ) , {
321+ status : 200 ,
322+ headers : { "Content-Type" : "application/json" } ,
323+ } ) ,
324+ ) ;
325+ }
326+ if (
327+ url === "/ai-router/api/accounts/auth/authorize" &&
328+ init ?. method === "POST"
329+ ) {
330+ authorizeCalls += 1 ;
331+ return Promise . resolve ( new Response ( "temporary unavailable" , { status : 502 } ) ) ;
332+ }
333+ return Promise . resolve ( new Response ( null , { status : 404 } ) ) ;
334+ } ) ;
335+ vi . stubGlobal ( "fetch" , fetchMock ) ;
336+
337+ renderAccountsPage ( ) ;
338+
339+ fireEvent . click ( await screen . findByRole ( "button" , { name : / 添 加 账 户 / } ) ) ;
340+ fireEvent . click ( await screen . findByText ( "官方账户" ) ) ;
341+ const officialModal = await screen . findByRole ( "dialog" , {
342+ name : "添加官方账户" ,
343+ } ) ;
344+ fireEvent . click (
345+ within ( officialModal ) . getByRole ( "button" , { name : "使用 ChatGPT 登录" } ) ,
346+ ) ;
347+
348+ await waitFor ( ( ) => {
349+ expect ( authorizeCalls ) . toBe ( 3 ) ;
350+ expect ( openExternalUrl ) . not . toHaveBeenCalled ( ) ;
351+ expect ( within ( officialModal ) . queryByText ( "设备码" ) ) . toBeNull ( ) ;
352+ expect (
353+ within ( officialModal ) . getByRole ( "button" , { name : "使用 ChatGPT 登录" } ) ,
354+ ) . toBeInTheDocument ( ) ;
355+ } ) ;
356+ } ) ;
357+
161358 it ( "supports official upload, third-party create, and chat test in a single dashboard" , async ( ) => {
162359 const accountList = [
163360 {
0 commit comments