@@ -100,3 +100,71 @@ describe('useAuth', () => {
100100 } ) ;
101101 } ) ;
102102} ) ;
103+
104+ describe ( 'session refresh (specs/behaviors/authorization.md → "The SPA keeps the session alive")' , ( ) => {
105+ const me = ( person : unknown ) =>
106+ new Response ( JSON . stringify ( { success : true , data : { person, accountLevel : person ? 'user' : 'anonymous' } } ) , {
107+ status : 200 ,
108+ headers : { 'content-type' : 'application/json' } ,
109+ } ) ;
110+ const PERSON = { id : 'p1' , slug : 'jane' , fullName : 'Jane Doe' , avatarUrl : null , accountLevel : 'user' } ;
111+
112+ afterEach ( ( ) => {
113+ document . cookie = 'cfp_has_session=; max-age=0; path=/' ;
114+ vi . restoreAllMocks ( ) ;
115+ } ) ;
116+
117+ it ( 'refreshes on load when the hint cookie says a session exists' , async ( ) => {
118+ document . cookie = 'cfp_has_session=1; path=/' ;
119+ let signedIn = false ;
120+ const fetchMock = vi . spyOn ( globalThis , 'fetch' ) . mockImplementation ( async ( input ) => {
121+ const url = String ( input ) ;
122+ if ( url === '/api/auth/refresh' ) {
123+ signedIn = true ;
124+ return new Response ( JSON . stringify ( { success : true , data : null } ) , { status : 200 } ) ;
125+ }
126+ return me ( signedIn ? PERSON : null ) ;
127+ } ) ;
128+
129+ renderWithRouter ( < Wrapped /> ) ;
130+ expect ( await screen . findByTestId ( 'user' ) ) . toHaveTextContent ( 'Jane Doe' ) ;
131+ expect ( fetchMock . mock . calls . filter ( ( [ u ] ) => String ( u ) === '/api/auth/refresh' ) ) . toHaveLength ( 1 ) ;
132+ } ) ;
133+
134+ it ( 'never refreshes for an anonymous visitor without the hint' , async ( ) => {
135+ const fetchMock = vi . spyOn ( globalThis , 'fetch' ) . mockImplementation ( async ( ) => me ( null ) ) ;
136+
137+ renderWithRouter ( < Wrapped /> ) ;
138+ expect ( await screen . findByTestId ( 'anon' ) ) . toBeInTheDocument ( ) ;
139+ expect ( fetchMock . mock . calls . some ( ( [ u ] ) => String ( u ) === '/api/auth/refresh' ) ) . toBe ( false ) ;
140+ } ) ;
141+
142+ it ( 'retries an API call once after refreshing on a 401' , async ( ) => {
143+ const { api } = await import ( '../src/lib/api.js' ) ;
144+ let refreshed = false ;
145+ const fetchMock = vi . spyOn ( globalThis , 'fetch' ) . mockImplementation ( async ( input ) => {
146+ const url = String ( input ) ;
147+ if ( url === '/api/auth/refresh' ) {
148+ refreshed = true ;
149+ return new Response ( JSON . stringify ( { success : true , data : null } ) , { status : 200 } ) ;
150+ }
151+ if ( ! refreshed ) {
152+ return new Response ( JSON . stringify ( { success : false , error : { code : 'unauthenticated' , message : 'no' } } ) , {
153+ status : 401 ,
154+ headers : { 'content-type' : 'application/json' } ,
155+ } ) ;
156+ }
157+ return new Response ( JSON . stringify ( { success : true , data : [ ] , metadata : { totalItems : 0 } } ) , {
158+ status : 200 ,
159+ headers : { 'content-type' : 'application/json' } ,
160+ } ) ;
161+ } ) ;
162+
163+ await api . projects . list ( { perPage : 1 } ) ;
164+ expect ( fetchMock . mock . calls . map ( ( [ u ] ) => String ( u ) ) ) . toEqual ( [
165+ '/api/projects?perPage=1' ,
166+ '/api/auth/refresh' ,
167+ '/api/projects?perPage=1' ,
168+ ] ) ;
169+ } ) ;
170+ } ) ;
0 commit comments