*** Settings *** Library Browser Library RequestsLibrary Library Collections Library String Documentation Shared auth keywords. Use Login As Admin for browser flows; ... Login Via HTTP for API-only assertions. Logout, Verify Session Invalid, ... Login With Wrong Password, and Login Exceeds Rate Limit are extended helpers. *** Keywords *** Login Via HTTP [Documentation] Login via HTTP and store session cookie for RequestsLibrary. ... Call this before any RequestsLibrary keyword that needs auth. ${headers}= Create Dictionary X-BanGUI-Request 1 IF "${XFF_HEADER}" != "" Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER} END Create Session bangsess ${BACKEND_URL} headers=${headers} ${login_payload}= Create Dictionary password ${TEST_PASSWORD} ${login_resp}= POST On Session bangsess /api/v1/auth/login ... json=${login_payload} ... expected_status=200 Log HTTP login done. cookies=${login_resp.cookies} RETURN bangsess Login As Admin [Documentation] Creates a new context and page and logs in via UI. ... Caller should NOT call New Context/New Page before this. # Check setup status via HTTP API. ${response}= GET ${BACKEND_URL}/api/v1/setup ${body}= Set Variable ${response.json()} Log Setup completed: ${body}[completed] IF not ${body}[completed] # Complete setup wizard via HTTP API. ${setup_payload}= Create Dictionary ... master_password=${TEST_PASSWORD} ... database_path=bangui.db ... fail2ban_socket=/var/run/fail2ban/fail2ban.sock ... timezone=UTC ... session_duration_minutes=${60} POST ${BACKEND_URL}/api/v1/setup json=${setup_payload} Log Setup POST completed. END # Create browser context. New Context New Page Go To ${FRONTEND_URL} Wait For Load State domcontentloaded # Wait for React to fully initialize before login attempt Sleep 5s # Use fetch to call login API with browser credentials so the session cookie # gets stored in the browser context. Use relative URL so Vite proxy handles it. ${login_result}= Evaluate JavaScript ${None} ... async () => { ... try { ... // Wait for React to fully initialize. ... await new Promise(r => setTimeout(r, 2000)); ... const res = await fetch('/api/v1/auth/login', { ... method: 'POST', ... headers: { 'Content-Type': 'application/json' }, ... body: JSON.stringify({ password: '${TEST_PASSWORD}' }), ... credentials: 'include' ... }); ... const data = await res.json().catch(() => ({})); ... return { ok: res.ok, status: res.status, data }; ... } catch(e) { ... return { ok: false, error: String(e) }; ... } ... } Log API login result: ${login_result} # Check if login actually succeeded before marking as authenticated ${login_ok}= Set Variable ${login_result}[ok] IF not ${login_ok} Fatal Error Login API failed: ${login_result} END # Set sessionStorage so AuthProvider considers us authenticated without waiting # for API re-validation on the next navigation. Evaluate JavaScript ${None} () => sessionStorage.setItem('bangui_authenticated', 'true') # Navigate directly to the dashboard instead of Reload. Reload causes a race # where useSessionValidation's API call may redirect to /login before main renders. # Going to / forces the SPA router to resolve routes while sessionStorage is already set. Go To ${FRONTEND_URL}/ Wait For Load State domcontentloaded # Poll for main to appear. The SPA remounts on navigation so domcontentloaded fires # before React has finished authenticating and rendering the protected route. ${login_ok}= Set Variable ${TRUE} FOR ${i} IN RANGE 1 16 ${url}= Get URL IF '/login' in '${url}' # Still on /login after navigation — login did not succeed. ${login_ok}= Set Variable ${FALSE} EXIT FOR LOOP END ${found}= Run Keyword And Return Status Wait For Elements State css=main visible timeout=2s IF ${found} BREAK END END IF not ${login_ok} ${last_result}= Set Variable ${login_result} Fatal Error Login failed: ${last_result} END ${final_url}= Get URL Log Login complete. URL: ${final_url} Logout [Documentation] Logs out the current browser session via UI Sign Out button. Click css=[aria-label="Sign out"] Wait For Load State domcontentloaded # Should land on /login. ${url}= Get URL Should Contain ${url} /login Verify Session Invalid [Documentation] Calls GET /api/v1/auth/session with no cookie. Must return 401. ${resp}= GET ${BACKEND_URL}/api/v1/auth/session expected_status=any Should Be Equal As Integers ${resp.status_code} 401 Login With Wrong Password [Documentation] Browser-driven: type a wrong password, expect error message. New Browser chromium headless=${TRUE} New Context New Page Go To ${FRONTEND_URL}/login Wait For Elements State css=input[type="password"] visible timeout=15s Fill Text css=input[type="password"] WrongPass99! Click css=button[type="submit"] # Expect to stay on /login. ${url}= Get URL Should Contain ${url} /login # Wait briefly for error to render. Sleep 2s # The MessageBar shows an error string. Assert at least one error-pattern element visible. ${error_visible}= Run Keyword And Return Status Wait For Elements State ... css=[role="alert"] visible timeout=5s Should Be True ${error_visible} msg=No error message shown for wrong password Close Browser Login Exceeds Rate Limit [Documentation] Posts 6 failed logins in a row from the same X-Forwarded-For. ... Expects 429 on at least one attempt (limit is 5/min/IP). Set Random Xff Header ${headers}= Create Dictionary ... X-BanGUI-Request 1 ... X-Forwarded-For ${XFF_HEADER} ... Content-Type application/json Create Session ratelim ${BACKEND_URL} headers=${headers} ${payload}= Create Dictionary password wrongpass1! ${got_429}= Set Variable ${FALSE} FOR ${i} IN RANGE 1 8 ${resp}= POST On Session ratelim /api/v1/auth/login ... json=${payload} expected_status=any Log Attempt ${i}: status=${resp.status_code} IF ${resp.status_code} == 429 ${got_429}= Set Variable ${TRUE} BREAK END Sleep 0.5 END Should Be True ${got_429} msg=Expected a 429 response after multiple failed logins Delete All Sessions