1- import { test , expect } from '@playwright/test' ;
1+ import { test , expect , Page , Route } from '@playwright/test' ;
22import { signInKeycloakIfNeeded } from '../utils-test' ;
3- import { getStorageStatePath } from '../utils' ;
43
54test . describe ( 'Authentication with empty storage state' , ( ) => {
65 test . use ( { storageState : { cookies : [ ] , origins : [ ] } } ) ;
@@ -17,3 +16,155 @@ test.describe('Authentication with existing storage state', () => {
1716 } ) ;
1817} ) ;
1918
19+ const SILENT_LOGIN_RETRY_KEY = 'messages_silent-login-retry' ;
20+
21+ const mockConfigApi = async ( page : Page , silentLoginEnabled : boolean ) => {
22+ await page . route ( '**/api/v1.0/config/' , async ( route : Route ) => {
23+ const response = await route . fetch ( ) ;
24+ const json = await response . json ( ) ;
25+ json . FRONTEND_SILENT_LOGIN_ENABLED = silentLoginEnabled ;
26+ await route . fulfill ( { response, json } ) ;
27+ } ) ;
28+ } ;
29+
30+ const getSilentLoginRetryKey = async ( page : Page ) : Promise < string | null > => {
31+ return page . evaluate (
32+ ( key ) => localStorage . getItem ( key ) ,
33+ SILENT_LOGIN_RETRY_KEY ,
34+ ) ;
35+ } ;
36+
37+ const clearSilentLoginRetryKey = async ( page : Page ) => {
38+ await page . evaluate (
39+ ( key ) => localStorage . removeItem ( key ) ,
40+ SILENT_LOGIN_RETRY_KEY ,
41+ ) ;
42+ } ;
43+
44+ // NOTE: Keycloak does not support silent login (prompt=none) when not running
45+ // behind HTTPS, so we cannot fully test the silent re-authentication flow in
46+ // this e2e environment. Instead, we verify that the correct requests are made
47+ // (redirect to /authenticate/?silent=true) and that the app handles the
48+ // outcome gracefully (showing the login page after a failed silent attempt).
49+ test . describe ( 'Silent Login' , ( ) => {
50+ test . use ( { storageState : { cookies : [ ] , origins : [ ] } } ) ;
51+
52+ test ( 'should attempt silent login with active Keycloak session' , async ( {
53+ page,
54+ browserName,
55+ } ) => {
56+ const username = `user.e2e.${ browserName } ` ;
57+
58+ // Sign in normally to establish a Keycloak session
59+ await signInKeycloakIfNeeded ( { page, username } ) ;
60+
61+ // Clear app cookies but preserve Keycloak session cookies
62+ const cookies = await page . context ( ) . cookies ( ) ;
63+ const keycloakCookies = cookies . filter ( ( c ) =>
64+ c . domain . includes ( 'keycloak' ) ,
65+ ) ;
66+ await page . context ( ) . clearCookies ( ) ;
67+ if ( keycloakCookies . length > 0 ) {
68+ await page . context ( ) . addCookies ( keycloakCookies ) ;
69+ }
70+
71+ // Clear localStorage retry key to allow silent login
72+ await clearSilentLoginRetryKey ( page ) ;
73+
74+ // Enable silent login by intercepting the config endpoint
75+ await mockConfigApi ( page , true ) ;
76+
77+ // Track the silent login redirect
78+ let silentLoginRequestMade = false ;
79+ page . on ( 'request' , ( request ) => {
80+ const url = request . url ( ) ;
81+ if ( url . includes ( '/authenticate/' ) && url . includes ( 'silent=true' ) ) {
82+ silentLoginRequestMade = true ;
83+ }
84+ } ) ;
85+
86+ // Navigate to the app - the silent login redirect should occur
87+ await page . goto ( '/' ) ;
88+
89+ // The login page is eventually shown because Keycloak returns a
90+ // "login_failed" error when not running behind HTTPS
91+ await expect ( page . locator ( 'button.pro-connect-button' ) ) . toBeVisible ( {
92+ timeout : 30000 ,
93+ } ) ;
94+
95+ // Verify the silent login redirect occurred with the expected parameters
96+ expect ( silentLoginRequestMade ) . toBe ( true ) ;
97+ } ) ;
98+
99+ test ( 'should fail gracefully without Keycloak session' , async ( {
100+ page,
101+ context,
102+ } ) => {
103+ // Ensure no session exists
104+ await context . clearCookies ( ) ;
105+
106+ // Enable silent login
107+ await mockConfigApi ( page , true ) ;
108+
109+ // Track the silent login redirect
110+ let silentLoginRequestMade = false ;
111+ page . on ( 'request' , ( request ) => {
112+ const url = request . url ( ) ;
113+ if ( url . includes ( '/authenticate/' ) && url . includes ( 'silent=true' ) ) {
114+ silentLoginRequestMade = true ;
115+ }
116+ } ) ;
117+
118+ // Navigate to the app
119+ await page . goto ( '/' ) ;
120+
121+ // Verify the ProConnect login button is shown (silent login failed,
122+ // retry key prevents re-attempt, login page displayed)
123+ await expect ( page . locator ( 'button.pro-connect-button' ) ) . toBeVisible ( {
124+ timeout : 30000 ,
125+ } ) ;
126+
127+ // Verify the silent login redirect occurred
128+ expect ( silentLoginRequestMade ) . toBe ( true ) ;
129+
130+ // Verify localStorage has the retry key set (preventing immediate retry)
131+ const retryKeyValue = await getSilentLoginRetryKey ( page ) ;
132+ expect ( retryKeyValue ) . not . toBeNull ( ) ;
133+ } ) ;
134+
135+ test ( 'should show login page directly when silent login is disabled' , async ( {
136+ page,
137+ context,
138+ } ) => {
139+ // Ensure no session exists
140+ await context . clearCookies ( ) ;
141+
142+ // Disable silent login
143+ await mockConfigApi ( page , false ) ;
144+
145+ // Track to verify NO silent login redirect
146+ let silentLoginRequestMade = false ;
147+ page . on ( 'request' , ( request ) => {
148+ const url = request . url ( ) ;
149+ if ( url . includes ( '/authenticate/' ) && url . includes ( 'silent=true' ) ) {
150+ silentLoginRequestMade = true ;
151+ }
152+ } ) ;
153+
154+ // Navigate to the app
155+ await page . goto ( '/' ) ;
156+
157+ // Verify the ProConnect login button is shown directly (no silent login attempt)
158+ await expect ( page . locator ( 'button.pro-connect-button' ) ) . toBeVisible ( {
159+ timeout : 10000 ,
160+ } ) ;
161+
162+ // Verify no silent login redirect was attempted
163+ expect ( silentLoginRequestMade ) . toBe ( false ) ;
164+
165+ // Verify no retry key in localStorage (silent login was never triggered)
166+ const retryKeyValue = await getSilentLoginRetryKey ( page ) ;
167+ expect ( retryKeyValue ) . toBeNull ( ) ;
168+ } ) ;
169+ } ) ;
170+
0 commit comments