11import { afterEach , describe , expect , test } from "bun:test" ;
2- import { withMockedModule } from "../../tests/helpers/mock-module.js" ;
3-
4- // getValidCodexToken/getValidXaiToken hit the real home-level auth store and
5- // refresh endpoints; stub the session layer so this test only exercises the
6- // scope probe's own HTTP call and status classification. Other suites
7- // (tests/unit/codex-session.test.ts) import the real modules directly, so the
8- // mocks must be torn down after this file's tests run rather than leaking
9- // into the rest of the bun test process.
10- await withMockedModule (
11- import . meta. resolve ( "./codex/session.js" ) ,
12- ( real : typeof import ( "./codex/session.js" ) ) => ( {
13- ...real ,
14- getValidCodexToken : async ( ) => ( { access : "codex-token" , accountId : "acct-1" } ) ,
15- } ) ,
16- ) ;
17- await withMockedModule (
18- import . meta. resolve ( "./xai/session.js" ) ,
19- ( real : typeof import ( "./xai/session.js" ) ) => ( {
20- ...real ,
21- getValidXaiToken : async ( ) => ( { access : "xai-token" } ) ,
22- } ) ,
23- ) ;
24-
25- const { checkOAuthProviderScope } = await import ( "./oauth-scope-check.js" ) ;
2+
3+ import { checkOAuthProviderScope } from "./oauth-scope-check.js" ;
264
275const originalFetch = global . fetch ;
6+ const codexTokens = {
7+ access : "staged-codex-token" ,
8+ refresh : "codex-refresh" ,
9+ expiresAt : Date . now ( ) + 3_600_000 ,
10+ accountId : "acct-staged" ,
11+ } ;
12+ const xaiTokens = {
13+ access : "staged-xai-token" ,
14+ refresh : "xai-refresh" ,
15+ expiresAt : Date . now ( ) + 3_600_000 ,
16+ } ;
2817
29- function stubFetch ( impl : ( url : string ) => Response | Promise < Response > ) : void {
30- global . fetch = ( async ( input : RequestInfo | URL ) => impl ( String ( input ) ) ) as typeof fetch ;
18+ function stubFetch ( impl : ( url : string , init ?: RequestInit ) => Response | Promise < Response > ) : void {
19+ global . fetch = ( async ( input : RequestInfo | URL , init ?: RequestInit ) =>
20+ impl ( String ( input ) , init ) ) as typeof fetch ;
3121}
3222
3323describe ( "checkOAuthProviderScope" , ( ) => {
3424 afterEach ( ( ) => {
3525 global . fetch = originalFetch ;
3626 } ) ;
3727
38- test ( "codex: ok when the catalog call succeeds" , async ( ) => {
39- stubFetch ( ( ) => new Response ( JSON . stringify ( { models : [ "gpt-5" ] } ) , { status : 200 } ) ) ;
40- const result = await checkOAuthProviderScope ( "codex" , "work" ) ;
28+ test ( "codex: builds the probe from staged tokens" , async ( ) => {
29+ stubFetch ( ( _url , init ) => {
30+ expect ( init ?. headers ) . toMatchObject ( {
31+ authorization : "Bearer staged-codex-token" ,
32+ "chatgpt-account-id" : "acct-staged" ,
33+ } ) ;
34+ return new Response ( JSON . stringify ( { models : [ "gpt-5" ] } ) , { status : 200 } ) ;
35+ } ) ;
36+ const result = await checkOAuthProviderScope ( "codex" , codexTokens ) ;
4137 expect ( result . status ) . toBe ( "ok" ) ;
4238 } ) ;
4339
40+ test ( "codex: refreshes expired staged tokens before classifying the probe" , async ( ) => {
41+ const expired = { ...codexTokens , expiresAt : 0 } ;
42+ const requests : string [ ] = [ ] ;
43+ stubFetch ( ( url , init ) => {
44+ requests . push ( url ) ;
45+ if ( url . includes ( "/oauth/token" ) ) {
46+ return new Response ( JSON . stringify ( { access_token : "refreshed-codex" , expires_in : 3600 } ) , {
47+ status : 200 ,
48+ headers : { "content-type" : "application/json" } ,
49+ } ) ;
50+ }
51+ expect ( init ?. headers ) . toMatchObject ( {
52+ authorization : "Bearer refreshed-codex" ,
53+ "chatgpt-account-id" : "acct-staged" ,
54+ } ) ;
55+ return new Response ( JSON . stringify ( { models : [ "gpt-5" ] } ) , { status : 200 } ) ;
56+ } ) ;
57+
58+ const result = await checkOAuthProviderScope ( "codex" , expired ) ;
59+
60+ expect ( result . status ) . toBe ( "ok" ) ;
61+ expect ( requests ) . toHaveLength ( 2 ) ;
62+ expect ( expired . access ) . toBe ( "refreshed-codex" ) ;
63+ } ) ;
64+
65+ test ( "codex: reports an expired staged token refresh failure as unavailable" , async ( ) => {
66+ const expired = { ...codexTokens , expiresAt : 0 } ;
67+ stubFetch ( ( ) => new Response ( "refresh rejected" , { status : 401 } ) ) ;
68+
69+ const result = await checkOAuthProviderScope ( "codex" , expired ) ;
70+
71+ expect ( result . status ) . toBe ( "unavailable" ) ;
72+ } ) ;
73+
4474 test ( "codex: insufficient-scope on a definitive 403" , async ( ) => {
4575 stubFetch ( ( ) => new Response ( "forbidden" , { status : 403 } ) ) ;
46- const result = await checkOAuthProviderScope ( "codex" , "work" ) ;
76+ const result = await checkOAuthProviderScope ( "codex" , codexTokens ) ;
4777 expect ( result . status ) . toBe ( "insufficient-scope" ) ;
4878 if ( result . status === "insufficient-scope" ) {
4979 expect ( result . message ) . toMatch ( / r e c o n n e c t / i) ;
@@ -54,41 +84,75 @@ describe("checkOAuthProviderScope", () => {
5484
5585 test ( "codex: insufficient-scope on a definitive 401" , async ( ) => {
5686 stubFetch ( ( ) => new Response ( "nope" , { status : 401 } ) ) ;
57- const result = await checkOAuthProviderScope ( "codex" , "work" ) ;
87+ const result = await checkOAuthProviderScope ( "codex" , codexTokens ) ;
5888 expect ( result . status ) . toBe ( "insufficient-scope" ) ;
5989 } ) ;
6090
6191 test ( "codex: unavailable on a network failure, not blocked" , async ( ) => {
6292 stubFetch ( ( ) => {
6393 throw new Error ( "fetch failed" ) ;
6494 } ) ;
65- const result = await checkOAuthProviderScope ( "codex" , "work" ) ;
95+ const result = await checkOAuthProviderScope ( "codex" , codexTokens ) ;
6696 expect ( result . status ) . toBe ( "unavailable" ) ;
6797 } ) ;
6898
6999 test ( "codex: unavailable (not scope failure) on a 500" , async ( ) => {
70100 stubFetch ( ( ) => new Response ( "boom" , { status : 500 } ) ) ;
71- const result = await checkOAuthProviderScope ( "codex" , "work" ) ;
101+ const result = await checkOAuthProviderScope ( "codex" , codexTokens ) ;
72102 expect ( result . status ) . toBe ( "unavailable" ) ;
73103 } ) ;
74104
75- test ( "xai: ok when the models call succeeds" , async ( ) => {
76- stubFetch ( ( ) => new Response ( JSON . stringify ( { data : [ ] } ) , { status : 200 } ) ) ;
77- const result = await checkOAuthProviderScope ( "xai" , "personal" ) ;
105+ test ( "xai: builds the probe from staged tokens" , async ( ) => {
106+ stubFetch ( ( _url , init ) => {
107+ expect ( init ?. headers ) . toMatchObject ( { authorization : "Bearer staged-xai-token" } ) ;
108+ return new Response ( JSON . stringify ( { data : [ ] } ) , { status : 200 } ) ;
109+ } ) ;
110+ const result = await checkOAuthProviderScope ( "xai" , xaiTokens ) ;
111+ expect ( result . status ) . toBe ( "ok" ) ;
112+ } ) ;
113+
114+ test ( "xai: refreshes expired staged tokens before classifying the probe" , async ( ) => {
115+ const expired = { ...xaiTokens , expiresAt : 0 } ;
116+ const requests : string [ ] = [ ] ;
117+ stubFetch ( ( url , init ) => {
118+ requests . push ( url ) ;
119+ if ( url . includes ( "/oauth2/token" ) ) {
120+ return new Response ( JSON . stringify ( { access_token : "refreshed-xai" , expires_in : 3600 } ) , {
121+ status : 200 ,
122+ headers : { "content-type" : "application/json" } ,
123+ } ) ;
124+ }
125+ expect ( init ?. headers ) . toMatchObject ( { authorization : "Bearer refreshed-xai" } ) ;
126+ return new Response ( JSON . stringify ( { data : [ ] } ) , { status : 200 } ) ;
127+ } ) ;
128+
129+ const result = await checkOAuthProviderScope ( "xai" , expired ) ;
130+
78131 expect ( result . status ) . toBe ( "ok" ) ;
132+ expect ( requests ) . toHaveLength ( 2 ) ;
133+ expect ( expired . access ) . toBe ( "refreshed-xai" ) ;
134+ } ) ;
135+
136+ test ( "xai: reports an expired staged token refresh failure as unavailable" , async ( ) => {
137+ const expired = { ...xaiTokens , expiresAt : 0 } ;
138+ stubFetch ( ( ) => new Response ( "refresh rejected" , { status : 401 } ) ) ;
139+
140+ const result = await checkOAuthProviderScope ( "xai" , expired ) ;
141+
142+ expect ( result . status ) . toBe ( "unavailable" ) ;
79143 } ) ;
80144
81145 test ( "xai: insufficient-scope on a definitive 403" , async ( ) => {
82146 stubFetch ( ( ) => new Response ( "forbidden" , { status : 403 } ) ) ;
83- const result = await checkOAuthProviderScope ( "xai" , "personal" ) ;
147+ const result = await checkOAuthProviderScope ( "xai" , xaiTokens ) ;
84148 expect ( result . status ) . toBe ( "insufficient-scope" ) ;
85149 } ) ;
86150
87151 test ( "xai: unavailable on a timeout-style abort" , async ( ) => {
88152 stubFetch ( ( ) => {
89153 throw new DOMException ( "The operation timed out." , "TimeoutError" ) ;
90154 } ) ;
91- const result = await checkOAuthProviderScope ( "xai" , "personal" ) ;
155+ const result = await checkOAuthProviderScope ( "xai" , xaiTokens ) ;
92156 expect ( result . status ) . toBe ( "unavailable" ) ;
93157 } ) ;
94158} ) ;
0 commit comments