*** Settings *** Resource ${CURDIR}/../resources/common.resource Resource ${CURDIR}/../resources/auth.resource # Use unique X-Forwarded-For to bypass per-IP rate limit across test runs. # Rate limit: 10 imports / IP / hour. Overridden at runtime via header. Suite Setup Login As Admin *** Test Cases *** Manual Blocklist Import Completes Without Error [Documentation] Verifies the full import pipeline: ... UI button click → async backend task → HTTP fetch → DB write → UI log entry. ... ... - Uses local mock server when external network is unavailable. ... - Rate limit bypassed via X-Forwarded-For header. ... - Import "completes successfully" is distinct from "bans were added". [Teardown] Cleanup Mock Server # Pre-condition: ensure at least one source is configured. ${sess}= Login Via HTTP Ensure Blocklist Source Exists ${sess} # Determine if external network is reachable. ${no_internet}= Evaluate __import__("socket").gethostbyname("one.one.one.one") is None modules=socket IF ${no_internet} Start Local Mock Server END # Navigate to blocklists page and locate the import button. Go To ${FRONTEND_URL}/blocklists Wait For Elements State css=button[data-testid="blocklist-import-button"] visible timeout=15s # Record current log entry count before triggering import. ${headers}= Create Dictionary X-Forwarded-For 10.0.0.99 ${resp_before}= GET On Session ${sess} /api/v1/blocklists/log headers=${headers} expected_status=200 ${log_count_before}= Get Length ${resp_before.json()}[items] # Trigger the import via the manual import button. Click css=button[data-testid="blocklist-import-button"] # Wait for import to finish: button re-enabled or success toast appears. Wait For Elements State css=button[data-testid="blocklist-import-button"] enabled timeout=45s # Assert no error banner in the UI. ${error_visible}= Run Keyword And Return Status Get Text css=[data-testid="error-banner"] contains error IF ${error_visible} ${error_text}= Get Text css=[data-testid="error-banner"] Fatal Error Import error banner appeared: ${error_text} END # Verify the log has a new entry (import ran, regardless of bans added). ${resp_after}= GET On Session ${sess} /api/v1/blocklists/log headers=${headers} expected_status=200 ${log_count_after}= Get Length ${resp_after.json()}[items] Should Be True ${log_count_after} > ${log_count_before} Log Import completed. Log entries: ${log_count_before} → ${log_count_after} *** Keywords *** Ensure Blocklist Source Exists [Arguments] ${sess} [Documentation] Guarantee at least one blocklist source exists. ... If GET /api/v1/blocklists returns an empty list, a minimal local-file ... source is added so the import test has a target. ${headers}= Create Dictionary X-Forwarded-For 10.0.0.99 ${resp}= GET On Session ${sess} /api/v1/blocklists headers=${headers} expected_status=200 ${sources}= Set Variable ${resp.json()}[sources] ${count}= Get Length ${sources} IF ${count} == 0 # No sources configured — add a minimal entry pointing to the mock server. # The mock server serves test.txt from the e2e directory. ${payload}= Create Dictionary ... name=Local Mock Source ... url=http://127.0.0.1:8765/test_blocklist.txt ... enabled=${TRUE} POST On Session ${sess} /api/v1/blocklists json=${payload} headers=${headers} expected_status=201 Log Created local mock blocklist source. ELSE Log Blocklist source already exists — using first available. END Start Local Mock Server [Documentation] Start a minimal Python HTTP server on port 8765 to serve a test blocklist file. ... The test.txt file contains one IP per line in plain-text format (fail2ban plain list). ${mock_file}= Set Variable ${CURDIR}/../../test_blocklist.txt ${file_exists}= OperatingSystem.File Should Exist ${mock_file} Start Process python -m http.server 8765 --bind 127.0.0.1 --directory ${CURDIR}/../../ alias=mockserver ... stdout=PIPE stderr=STDOUT Sleep 2s Log Local mock HTTP server started on 127.0.0.1:8765. Cleanup Mock Server [Documentation] Stop the mock HTTP server started by Start Local Mock Server. ${status}= Run Keyword And Return Status Terminate Process mockserver Log Mock server cleanup: ${status}