Files
BanGUI/e2e/resources/auth.resource
Lukas 0d21e3253e test(e2e): split suite by feature area with shared resources
Restructure 5 existing .robot files into 10 numbered files, one per
feature area in Docs/Features.md. Each file is independently runnable.
Add api.resource + data.resource for CSRF/XFF-aware wrappers and
RFC5737 IP generators.

Coverage: 110 new tests across login, dashboard, map, jails, config,
history, blocklists, layout. Uses existing data-testid/aria-label/role
selectors only — no frontend changes.

Tests bypass per-IP rate limits via X-Forwarded-For header rotation.
Hard rule preserved: failures are findings, never app-code fixes.
2026-06-21 07:55:19 +02:00

171 lines
7.1 KiB
Plaintext

*** 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