23 KiB
[E2E-1] Set up Robot Framework E2E test infrastructure
Where found: No E2E test suite exists in the repository. There are 87 backend pytest files and 78 frontend vitest files but zero integration/E2E tests that exercise the full running stack.
Why this is needed: Unit and component tests cannot catch regressions that span the full system: frontend → backend → fail2ban → database. A running-stack test suite is the only safety net for deployment-breaking changes.
Goal:
Create the e2e/ directory layout, install Robot Framework with the Browser library (Playwright-backed), configure shared keywords for startup/teardown and authentication, and add a make e2e target so the suite can be run with a single command.
What to do:
- Create
e2e/requirements.txt:robotframework>=7 robotframework-browser>=18 - Run
rfbrowser initafter install to download Playwright browsers. - Create the directory layout:
e2e/ ├── requirements.txt ├── resources/ │ ├── common.resource # variables, shared setup/teardown │ └── auth.resource # Login As Admin keyword └── tests/ ├── 01_page_loading.robot ├── 02_ban_records.robot ├── 03_blocklist_import.robot └── 04_config_edit.robot common.resourcemust define:${FRONTEND_URL}=http://localhost:5173${BACKEND_URL}=http://localhost:8000- Suite Setup: wait for
GET ${BACKEND_URL}/api/healthto return 200 before any test runs (poll with timeout 120 s).
auth.resourcemust implementLogin As Admin:- Check
GET /api/setup/status; if setup is not done, complete the setup wizard first. - POST credentials to
/api/auth/loginor drive the login form at/login. - Store the resulting session for subsequent page navigations.
- Check
- Add to
Makefile:e2e: up @echo "Waiting for stack to be healthy…" @sleep 60 robot --outputdir e2e/results e2e/tests/ - Add
e2e/results/to.gitignore.
Possible traps and issues:
BANGUI_SESSION_SECRETenv var is required; tests will fail with a startup error if it is not set. Document thatmake e2erequires the variable in the environment.make upusespodman-composeorpodman composeauto-detected at Makefile eval time. If neither is installed thee2etarget silently fails.- The backend
start_periodin the healthcheck is 45 s; the frontend is 30 s on top of that. The 60 s sleep in the Makefile target may not be enough on a cold build — prefer polling/api/healthuntil ready. rfbrowser initmust be re-run whenever therobotframework-browserversion changes.- The Browser library uses Chromium headless by default. CI environments may need
--no-sandboxflags passed viaNew Browser chromium headless=true args=['--no-sandbox'].
Docs changes needed:
- Add an "E2E Testing" section to Testing-Requirements.md describing how to run
make e2e, required env vars, and how to view the HTML report ine2e/results/. - Add
e2e/results/to the.gitignorelist documented in Backend-Development.md.
Doc references:
- Testing-Requirements.md
- Backend-Development.md
- Deployment.md — for env var documentation
- Robot Framework: https://robotframework.org/#getting-started
- Browser library: https://robotframework-browser.org/
[E2E-2] Page loading tests — all routes render without error
Where found:
The frontend has eight distinct routes (/setup, /login, /, /map, /jails, /jails/:name, /config, /history, /blocklists). Each is wrapped in a PageErrorBoundary. There is no test that verifies all of them load successfully against a live stack.
Why this is needed: A broken import, a missing API field, or a bad runtime dependency can cause a page to show the error boundary fallback ("Something went wrong") instead of its content. Unit tests mock API responses, so they cannot catch this class of regression.
Goal:
Every protected page loads, shows its primary content element, and does not show the PageErrorBoundary fallback when the stack is running correctly.
What to do:
- Create
e2e/tests/01_page_loading.robot. - Suite Setup: call
Login As Adminfromauth.resource. - For each route, implement a test case with the pattern:
*** Test Cases *** Login Page Loads Without Error # Must run before Login As Admin — navigate while unauthenticated Go To ${FRONTEND_URL}/login Wait For Elements State css=form visible timeout=15s Get Text body not contains Something went wrong Dashboard Loads Without Error Go To ${FRONTEND_URL}/ Wait For Elements State css=main visible timeout=15s Get Text body not contains Something went wrong Map Page Loads Without Error Go To ${FRONTEND_URL}/map Wait For Elements State css=canvas,svg visible timeout=15s Get Text body not contains Something went wrong - Cover all routes:
/setup— assert setup form OR redirect to/login(setup already done)./login— assert login form visible./— assert dashboard stats/charts visible./map— assert SVG or canvas element visible./jails— assert a table or list visible./jails/:name— navigate to/jails/manual-Jail; assert jail detail heading visible./config— assert tab navigation visible./history— assert history table visible./blocklists— assert blocklists panel visible.
- Assert HTTP status for each page via
${response}= GET ${FRONTEND_URL}/<path>andShould Be Equal As Integers ${response.status} 200.
Possible traps and issues:
- The
/loginpage test must run beforeLogin As Adminis called, or the session cookie will cause an immediate redirect to/. Either make it the first test case with its own[Setup] New Page(no auth), or run it in a separate suite that has no Suite Setup. - The frontend is a SPA;
GET /mapat the Vite dev server always returns 200 withindex.html. HTTP status checks here are not meaningful — focus on DOM assertions after client-side routing. - The
/jails/:nametest assumesmanual-Jailexists. If fail2ban has not started or the jail is not active the page may render an empty or error state. Add a guard: check jail exists viaGET /api/jailsbefore navigating. PageErrorBoundaryrenders per-page; the text "Something went wrong" must not be matched against the window title or other benign text. Scope the assertion to the<main>element.- Page elements have no
data-testidattributes on the production components — only on test mocks. CSS selectors (css=main,css=table,css=canvas) are fragile. See [E2E-6] for the task to adddata-testidattributes. - The Vite dev server takes ~30 s to compile on first load. The first navigation may time out; increase the default timeout to 30 s for the first test only.
Docs changes needed:
- Document the expected selectors and page landmarks in Web-Development.md so frontend developers know which elements are load-tested.
Doc references:
- Web-Development.md
- Web-Design.md
- Testing-Requirements.md
frontend/src/App.tsx— canonical route definitions
[E2E-3] Ban records appear in UI after simulated failed logins
Where found:
Docker/simulate_failed_logins.sh exists and is used in the make dev-ban-test target, but there is no automated test that verifies the resulting bans are surfaced correctly in the BanGUI frontend (History page, Dashboard, or Jails detail page).
Why this is needed: The ban pipeline is the core feature of the product. A regression anywhere in the chain (fail2ban log parsing → fail2ban banning → backend polling → database write → API response → frontend rendering) would go undetected until a user reports it.
Goal:
After running simulate_failed_logins.sh with a known IP, the ban record for that IP must appear in the BanGUI UI within a reasonable timeout.
What to do:
- Create
e2e/tests/02_ban_records.robot. - Suite Setup:
Login As Admin. - Test teardown: unban the test IP using
Docker/check_ban_status.sh --unban 192.168.100.99viaRun Process. - Test case:
*** Test Cases *** Simulated Failed Logins Appear As Ban Records [Teardown] Run Process bash ${EXECDIR}/Docker/check_ban_status.sh ... --unban 192.168.100.99 # Step 1 — write failure lines ${result}= Run Process ... bash ${EXECDIR}/Docker/simulate_failed_logins.sh 5 192.168.100.99 ... timeout=15s Should Be Equal As Integers ${result.rc} 0 # Step 2 — wait for fail2ban to process and backend to pick up the ban Sleep 15s # Step 3 — check History page Go To ${FRONTEND_URL}/history Wait For Elements State css=table,tbody visible timeout=20s Get Text body contains 192.168.100.99 # Step 4 — confirm jail name is shown Get Text body contains manual-Jail - Optionally add a direct API assertion before the UI check to isolate UI vs. backend failures:
${resp}= GET ${BACKEND_URL}/api/history expected_status=200 Should Contain ${resp.text} 192.168.100.99
Possible traps and issues:
- The default
COUNTforsimulate_failed_logins.shis 5 and the fail2banmaxretryformanual-Jailmust be ≤ 5 for the ban to trigger. If the jail config has been changed locally the test may pass the script step but produce no ban. simulate_failed_logins.shwrites toDocker/logs/auth.log. The fail2ban container reads from/remotelogs/bangui/auth.log(mapped volume). If the file mapping differs the lines will never be detected.- The backend polls fail2ban on a schedule (APScheduler). The 15 s sleep may not be enough if the polling interval is longer. Read the scheduler interval from the config before hardcoding the wait.
check_ban_status.shusesdocker execdirectly. In a Podman environment the container runtime may bepodman, making the unban teardown fail and leaving the test IP permanently banned until manual cleanup.- The History page paginates results. If the test IP is not on the first page the
containsassertion will fail. Assert via the API or set a large page size query parameter in the URL. - Running the test suite multiple times in the same session accumulates lines in
auth.log. Old lines do not re-trigger bans, but the counter inside fail2ban resets on container restart. Add atruncate -s 0 Docker/logs/auth.logstep before writing new lines if idempotency is needed.
Docs changes needed:
- Add a note in Testing-Requirements.md explaining the ban pipeline timing expectations and why a sleep is needed in this test.
- Document the
manual-Jailmaxretry value and log path in CONFIGURATION.md so it is clear what the E2E test depends on.
Doc references:
- CONFIGURATION.md
- Testing-Requirements.md
Docker/simulate_failed_logins.shDocker/check_ban_status.shDocker/fail2ban-dev-config/— jail configuration
[E2E-4] Blocklist import executes and is reflected in the UI
Where found:
The blocklist import endpoint (POST /api/blocklists/import) is implemented in backend/app/routers/blocklist.py and has unit tests in backend/tests/test_routers/test_blocklist.py and backend/tests/test_tasks/test_blocklist_import.py. The /blocklists frontend page exists but there is no E2E test verifying the manual import button works end-to-end.
Why this is needed: The import flow is asynchronous and involves DNS validation, an external HTTP fetch, and a database write. Unit tests mock all external calls. An E2E test is the only way to verify that the full import pipeline — including the network call to the external source — completes and updates the UI.
Goal:
Clicking the manual import button on the /blocklists page triggers an import run, the UI reflects completion (no error banner), and the import log table shows a new entry.
What to do:
- Create
e2e/tests/03_blocklist_import.robot. - Suite Setup:
Login As Admin. - Pre-condition: at least one blocklist source must be configured. Add a keyword
Ensure Blocklist Source Existsthat:- Calls
GET /api/blockliststo check if any sources are defined. - If none: calls
POST /api/blockliststo add a known-good source (e.g., a small, stable public list).
- Calls
- Test case:
*** Test Cases *** Manual Blocklist Import Completes Without Error Ensure Blocklist Source Exists Go To ${FRONTEND_URL}/blocklists Wait For Elements State css=[aria-label*="Import"],button visible timeout=15s # record the current log entry count ${resp_before}= GET ${BACKEND_URL}/api/blocklists/log expected_status=200 # trigger the import Click css=[aria-label*="Import"],button # wait for import to finish (spinner gone or success toast) Wait For Elements State css=[aria-label*="Import"],button enabled timeout=45s Get Text body not contains error # verify the log has a new entry ${resp_after}= GET ${BACKEND_URL}/api/blocklists/log expected_status=200 Should Not Be Equal ${resp_before.text} ${resp_after.text} - If the environment has no internet access, mock the external fetch via a local HTTP server (use Python's
http.serverin aRun Processbackground task and point the source URL athttp://localhost:<port>/test.txt).
Possible traps and issues:
- The import endpoint has a rate limit (
RATE_LIMIT_BLOCKLIST_IMPORT_REQUESTS). Running the suite more than once in the same hour may result in a 429. Either reset the rate limiter between runs or use a unique client IP via a customX-Forwarded-Forheader. - The external network may be unavailable in CI. The test must either skip gracefully (
Skip If ${no_internet}) or use a local mock server. - The import button selector is unknown until inspected in a running browser. The selector
css=[aria-label*="Import"],buttonis a best guess — it must be verified against the actual rendered DOM and updated if wrong. - If a blocklist source URL returns an invalid or empty file, the import will complete but with 0 bans added. The test must distinguish "import ran successfully" from "bans were added" — these are separate assertions.
- The
POST /api/blocklists/importcan take several seconds per source. The 45 s timeout may be insufficient for large lists.
Docs changes needed:
- Add the rate limit value and reset mechanism to CONFIGURATION.md.
- Document the E2E dependency on network access (or local mock) in Testing-Requirements.md.
Doc references:
- CONFIGURATION.md
- Testing-Requirements.md
- Features.md — blocklist feature description
backend/app/routers/blocklist.py— endpoint and rate limit implementationbackend/tests/test_tasks/test_blocklist_import.py
[E2E-5] Config edit saves and persists after page reload
Where found:
The /config page allows editing jail settings, filter definitions, and server-level options. It is covered by unit tests (frontend/src/pages/__tests__/ConfigPage.test.tsx, backend/tests/test_routers/test_config.py) but no E2E test verifies that a change made in the UI actually persists through the backend, survives a page reload, and reflects the new value.
Why this is needed:
The config page uses an auto-save mechanism (useAutoSave) that debounces writes. A regression in the debounce logic, the PATCH endpoint, or the GET-on-mount rehydration would silently discard user edits. Only a full round-trip test can catch this.
Goal: Change a config field value via the UI, wait for the auto-save indicator to confirm the save, reload the page, and assert the new value is still present.
What to do:
- Create
e2e/tests/04_config_edit.robot. - Suite Setup:
Login As Admin. - Choose a safe, low-risk config field to edit — e.g., the
[DEFAULT]bantimevalue, or a per-jailmaxretrysetting. - Record the original value before editing so it can be restored in teardown.
- Test case:
*** Settings *** Library Browser Resource ../resources/auth.resource Test Teardown Restore Original Config Value *** Test Cases *** Config Field Edit Persists After Reload Login As Admin Go To ${FRONTEND_URL}/config Wait For Elements State css=[role="tablist"] visible timeout=15s # Read current value for teardown ${original}= Get Text css=[data-field="bantime"] Set Suite Variable ${ORIGINAL_BANTIME} ${original} # Edit the field Fill Text css=[data-field="bantime"] 7200 # Wait for auto-save indicator to show "Saved" Wait For Elements State css=[data-autosave="saved"] visible timeout=15s # Reload and verify persistence Reload Wait For Elements State css=[data-field="bantime"] visible timeout=15s Get Text css=[data-field="bantime"] == 7200 *** Keywords *** Restore Original Config Value Go To ${FRONTEND_URL}/config Fill Text css=[data-field="bantime"] ${ORIGINAL_BANTIME} Wait For Elements State css=[data-autosave="saved"] visible timeout=15s - Also verify via the API that the value was actually written:
${resp}= GET ${BACKEND_URL}/api/config expected_status=200 Should Contain ${resp.text} 7200
Possible traps and issues:
- The config page auto-save uses a debounce delay. The test must wait for the "Saved" indicator rather than a fixed
Sleep, otherwise the reload may happen before the PATCH request fires. - The selectors
[data-field="bantime"]and[data-autosave="saved"]do not exist in the current frontend components (nodata-*attributes on production elements). These must be added to the components before the test can work. See [E2E-6] for the prerequisite task. - Config fields are rendered inside a tab panel. The correct tab must be activated before the target field is interactable. The test must click the right tab first.
- If the backend validates the new value and rejects it (e.g., bantime must be a positive integer), the test will fail at the API assertion. Use a value that is guaranteed to be valid.
- Editing config files on disk via the API may restart the fail2ban service inside the container, causing a brief health-check failure and destabilising subsequent tests in the suite. Run config edit tests last or use a test-only jail that is isolated from the main config.
- Teardown must restore the original value even if the test fails mid-way. Ensure
Test Teardownis set, not just a final keyword call.
Docs changes needed:
- Document the auto-save debounce behaviour and the "Saved" indicator semantics in Web-Development.md so E2E test authors know what to wait for.
- Note in Testing-Requirements.md that config edit tests must restore state in teardown.
Doc references:
- Web-Development.md
- CONFIGURATION.md
- Testing-Requirements.md
frontend/src/components/config/__tests__/AutoSaveIndicator.test.tsxfrontend/src/hooks/__tests__/useAutoSave.test.tsbackend/tests/test_routers/test_config.py
[E2E-6] Add data-testid / data-* attributes to production frontend components
Where found:
Inspecting the frontend source, data-testid attributes appear only in test mock files (e.g., MapPage.test.tsx line 57: <div data-testid="world-map" />). The production components in frontend/src/components/ and frontend/src/pages/ have no data-testid or data-* attributes. E2E tests [E2E-2], [E2E-4], and [E2E-5] all require stable selectors that survive CSS and class-name refactors.
Why this is needed:
CSS class selectors and aria-label text are brittle — they break when styles change or text is translated. data-testid attributes are the idiomatic, refactor-safe way to locate elements in E2E tests. Without them, every UI change risks breaking the E2E suite for reasons unrelated to correctness.
Goal:
Key interactive and landmark elements across all pages and the config form have data-testid (or semantic data-*) attributes that the Robot Framework E2E suite can rely on.
What to do:
- Identify the minimum set of elements needed by the four E2E suites:
data-testid="page-error-boundary"on thePageErrorBoundaryfallback render.data-testid="dashboard",data-testid="map-page",data-testid="jails-page",data-testid="history-page",data-testid="blocklists-page",data-testid="config-page"on each page's root element.data-testid="history-table"on the History page table body.data-testid="blocklist-import-button"on the manual import trigger.data-testid="autosave-status"on the auto-save indicator, withdata-status="saved" | "saving" | "error".data-field="<fieldname>"on config input fields that E2E tests will edit.
- Add the attributes directly to the JSX — no wrappers, no extra elements.
- Do not add
data-testidto elements that already have stable semantic roles (e.g.,<button type="submit">with unique text, landmark<main>,<nav>). - Update the existing vitest component tests to use the new
data-testidselectors instead of text queries where appropriate.
Possible traps and issues:
data-testidattributes are visible in production HTML. This is standard practice and not a security concern, but some teams prefer to strip them in production builds via a Babel/Vite plugin. Decide on a policy before adding them.- Adding attributes to components that are also tested by vitest may require updating those unit tests if they query by
data-testid. - The
PageErrorBoundarycomponent wraps lazy-loaded pages. The fallback element must carry thedata-testidon the rendered fallback JSX, not on the wrapper itself. - Config fields are rendered dynamically from API data. The
data-fieldvalue must be derived from the field's API key name, not a hardcoded index.
Docs changes needed:
- Add a "Selector conventions" section to Web-Development.md documenting when to use
data-testidvs. semantic selectors vs. ARIA roles, and listing all reserveddata-testidvalues used by the E2E suite.
Doc references:
- Web-Development.md
- Testing-Requirements.md
frontend/src/components/ErrorBoundary.tsxfrontend/src/components/config/__tests__/AutoSaveIndicator.test.tsx