Compare commits
23 Commits
408eb900eb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 42d5c2a01f | |||
| db17f3571b | |||
| cbddebf3b8 | |||
| 38d1594d21 | |||
| 2538c50321 | |||
| 5f33959efd | |||
| 848531c134 | |||
| 0d21e3253e | |||
| 3af8f0571b | |||
| d5a78a251a | |||
| 904db63fa2 | |||
| d737a1c319 | |||
| 9e765c6cb7 | |||
| ecb8542496 | |||
| 97f4df4a61 | |||
| 44542b93c0 | |||
| 01a4215f60 | |||
| bc49b7cd5b | |||
| fa4fe4bbdf | |||
| ee0fe9c695 | |||
| 551db0bb9c | |||
| 4a649e7347 | |||
| 025c82a982 |
@@ -48,6 +48,7 @@ services:
|
|||||||
target: runtime
|
target: runtime
|
||||||
container_name: bangui-backend
|
container_name: bangui-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
stop_grace_period: 30s # Give lifespan 30s to complete before SIGKILL
|
||||||
depends_on:
|
depends_on:
|
||||||
fail2ban:
|
fail2ban:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
1965
Docs/Tasks.md
1965
Docs/Tasks.md
File diff suppressed because it is too large
Load Diff
@@ -102,7 +102,7 @@ for (int i = 0; i < items.Count; i++)
|
|||||||
|
|
||||||
// Step 1 — run the task prompt
|
// Step 1 — run the task prompt
|
||||||
await RunCopilot(Enumerable.Empty<string>(), $"/caveman full");
|
await RunCopilot(Enumerable.Empty<string>(), $"/caveman full");
|
||||||
await RunCopilot(new[] { "--continue" }, $"read ./Docs/Instructions.md. fix the following test and only that one. Keep in mind that i did many refactorings and test may is obsolet or need to be changed. {item}");
|
await RunCopilot(new[] { "--continue" }, $"read ./Docs/Instructions.md. fix the following test and only that one. {item}");
|
||||||
if (cts.IsCancellationRequested) break;
|
if (cts.IsCancellationRequested) break;
|
||||||
|
|
||||||
// Step 2 — confirm completion in the same chat session
|
// Step 2 — confirm completion in the same chat session
|
||||||
|
|||||||
@@ -274,7 +274,18 @@ CREATE INDEX IF NOT EXISTS idx_import_log_source_id_desc
|
|||||||
|
|
||||||
|
|
||||||
async def _configure_connection(db: aiosqlite.Connection) -> None:
|
async def _configure_connection(db: aiosqlite.Connection) -> None:
|
||||||
"""Apply hardening pragmas to a newly-opened SQLite connection."""
|
"""Apply hardening pragmas to a newly-opened SQLite connection.
|
||||||
|
|
||||||
|
WAL mode is intentionally kept despite the risk of orphaned ``.wal``/``.shm``
|
||||||
|
files after unclean shutdowns. The benefits for concurrent readers
|
||||||
|
(readers do not block writers) outweigh the cleanup overhead, especially
|
||||||
|
under load. BanGUI runs as a single worker, but multiple concurrent HTTP
|
||||||
|
requests can still issue overlapping reads; DELETE mode would serialize
|
||||||
|
those reads behind any write, degrading API performance.
|
||||||
|
|
||||||
|
Orphaned files are handled by :func:`_cleanup_wal_files`, which is called
|
||||||
|
during startup before the database is opened.
|
||||||
|
"""
|
||||||
await db.execute("PRAGMA journal_mode=WAL;")
|
await db.execute("PRAGMA journal_mode=WAL;")
|
||||||
await db.execute("PRAGMA foreign_keys=ON;")
|
await db.execute("PRAGMA foreign_keys=ON;")
|
||||||
await db.execute("PRAGMA busy_timeout=5000;")
|
await db.execute("PRAGMA busy_timeout=5000;")
|
||||||
|
|||||||
@@ -318,7 +318,12 @@ async def _lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
log.error("scheduler_lock_release_failed", error=str(e))
|
log.error("scheduler_lock_release_failed", error=str(e))
|
||||||
|
|
||||||
# 6. Close the database connection.
|
# 6. Close the database connection.
|
||||||
|
try:
|
||||||
await startup_db.close()
|
await startup_db.close()
|
||||||
|
log.debug("database_connection_closed")
|
||||||
|
except Exception as exc:
|
||||||
|
log.error("database_connection_close_failed", error=str(exc))
|
||||||
|
|
||||||
log.info("bangui_shut_down")
|
log.info("bangui_shut_down")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,9 @@ from pathlib import Path
|
|||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from app.utils.logging_compat import get_logger
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler # type: ignore[import-untyped]
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler # type: ignore[import-untyped]
|
||||||
|
|
||||||
from app.db import init_db, open_db
|
from app.db import _cleanup_wal_files, init_db, open_db
|
||||||
from app.services import setup_service
|
from app.services import setup_service
|
||||||
from app.services.dns_validated_connector import create_dns_validated_socket_factory
|
from app.services.dns_validated_connector import create_dns_validated_socket_factory
|
||||||
from app.services.geo_cache import GeoCache
|
from app.services.geo_cache import GeoCache
|
||||||
@@ -48,6 +47,7 @@ from app.tasks import (
|
|||||||
from app.utils.async_utils import run_blocking
|
from app.utils.async_utils import run_blocking
|
||||||
from app.utils.fail2ban_db_utils import ensure_fail2ban_indexes
|
from app.utils.fail2ban_db_utils import ensure_fail2ban_indexes
|
||||||
from app.utils.jail_config import ensure_jail_configs
|
from app.utils.jail_config import ensure_jail_configs
|
||||||
|
from app.utils.logging_compat import get_logger
|
||||||
from app.utils.runtime_state import set_runtime_settings
|
from app.utils.runtime_state import set_runtime_settings
|
||||||
from app.utils.scheduler_lock import (
|
from app.utils.scheduler_lock import (
|
||||||
acquire_scheduler_lock,
|
acquire_scheduler_lock,
|
||||||
@@ -98,9 +98,7 @@ def _check_single_worker_mode() -> None:
|
|||||||
"See Docs/Architekture.md § Deployment Constraints for details."
|
"See Docs/Architekture.md § Deployment Constraints for details."
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(f"BANGUI_WORKERS environment variable must be an integer, got: {workers_env}") from e
|
||||||
f"BANGUI_WORKERS environment variable must be an integer, got: {workers_env}"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
|
|
||||||
async def _ensure_database_schema(database_path: str) -> None:
|
async def _ensure_database_schema(database_path: str) -> None:
|
||||||
@@ -333,6 +331,11 @@ async def _stage_init_database(app: FastAPI, settings: Settings) -> Any:
|
|||||||
|
|
||||||
log.debug("database_directory_ensured", directory=str(db_path.parent))
|
log.debug("database_directory_ensured", directory=str(db_path.parent))
|
||||||
|
|
||||||
|
# Clean up orphaned WAL files from previous unclean shutdowns before
|
||||||
|
# opening the database. This prevents stale .wal/.shm files from
|
||||||
|
# interfering with startup or triggering misleading warnings.
|
||||||
|
await _cleanup_wal_files(settings.database_path)
|
||||||
|
|
||||||
original_db_path = db_path.resolve()
|
original_db_path = db_path.resolve()
|
||||||
startup_db = await open_db(settings.database_path)
|
startup_db = await open_db(settings.database_path)
|
||||||
|
|
||||||
@@ -357,9 +360,7 @@ async def _stage_init_database(app: FastAPI, settings: Settings) -> Any:
|
|||||||
if f2b_db_path:
|
if f2b_db_path:
|
||||||
await run_blocking(ensure_fail2ban_indexes, f2b_db_path)
|
await run_blocking(ensure_fail2ban_indexes, f2b_db_path)
|
||||||
|
|
||||||
persisted_runtime_settings = (
|
persisted_runtime_settings = await setup_service.get_persisted_runtime_settings(runtime_db)
|
||||||
await setup_service.get_persisted_runtime_settings(runtime_db)
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
await runtime_db.close()
|
await runtime_db.close()
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,38 @@
|
|||||||
# E2E Tests — Running Robot Framework Tests
|
# E2E Tests — Running Robot Framework Tests
|
||||||
|
|
||||||
|
## Test File Structure
|
||||||
|
|
||||||
|
The E2E suite is organized **one `.robot` file per feature area** defined in `Docs/Features.md`. Each file is independently runnable.
|
||||||
|
|
||||||
|
| File | Feature |
|
||||||
|
|---|---|
|
||||||
|
| `01_setup_and_auth.robot` | Setup wizard (formerly `05_setup.robot`) — form fields, password strength, validation, full submit |
|
||||||
|
| `02_login.robot` | Login page — wrong password, rate limit (429), session validation 401, logout |
|
||||||
|
| `03_dashboard.robot` | Ban Overview (Dashboard) — status bar, time-range presets, data-source badges, API endpoints |
|
||||||
|
| `04_map.robot` | World Map View — country fills, click-to-filter, zoom controls, sticky table header/footer |
|
||||||
|
| `05_jails.robot` | Jail Management — list, ban/unban API, IP lookup, ignore list, jail controls |
|
||||||
|
| `06_config_jails_filters_actions.robot` | Configuration View — Jails/Filters/Actions tabs, inline edit, raw config, regex tester |
|
||||||
|
| `07_config_log_and_serversettings.robot` | Server settings + log viewer + log observation allowlist |
|
||||||
|
| `08_history.robot` | Ban History — table, filters, per-IP timeline, archive vs fail2ban source |
|
||||||
|
| `09_blocklists.robot` | External Blocklist Importer — CRUD, SSRF validation, schedule, import log, delete restriction |
|
||||||
|
| `10_general_layout.robot` | General UI/layout — sidebar nav, theme toggle, session persistence, health endpoints |
|
||||||
|
| `02_ban_records.robot` | (pre-existing) end-to-end ban pipeline: fail2ban log → history |
|
||||||
|
| `03_blocklist_import.robot` | (pre-existing) blocklist manual import via UI |
|
||||||
|
| `04_config_edit.robot` | (pre-existing) config field auto-save round trip |
|
||||||
|
|
||||||
|
## Resource Files
|
||||||
|
|
||||||
|
Shared keywords live in `resources/`:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `common.resource` | `Wait For Backend Health`, `Wait For Frontend`, `Page Should Contain` wrapper, `XFF` helpers, IP/jail name generators |
|
||||||
|
| `auth.resource` | `Login As Admin`, `Login Via HTTP`, `Logout`, `Verify Session Invalid`, `Login With Wrong Password`, `Login Exceeds Rate Limit` |
|
||||||
|
| `api.resource` | `Api Get/Post/Put/Delete` wrappers that auto-inject CSRF + X-Forwarded-For headers |
|
||||||
|
| `data.resource` | Unique IP / jail name / blocklist name generators (RFC5737 ranges) |
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Install dependencies:
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
rfbrowser init
|
rfbrowser init
|
||||||
@@ -14,10 +44,17 @@ rfbrowser init
|
|||||||
robot --outputdir results --log log.html --report report.html tests/
|
robot --outputdir results --log log.html --report report.html tests/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or via the Makefile from the repo root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make e2e
|
||||||
|
```
|
||||||
|
|
||||||
## Run Specific Test File
|
## Run Specific Test File
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
robot --outputdir results tests/01_page_loading.robot
|
robot --outputdir results tests/02_login.robot
|
||||||
|
robot --outputdir results tests/08_history.robot
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run with Browser Visible
|
## Run with Browser Visible
|
||||||
@@ -26,10 +63,42 @@ robot --outputdir results tests/01_page_loading.robot
|
|||||||
robot --outputdir results --variable BROWSER:chromium tests/
|
robot --outputdir results --variable BROWSER:chromium tests/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Rate-Limit Workaround
|
||||||
|
|
||||||
|
BanGUI rate-limits several endpoints per source IP:
|
||||||
|
|
||||||
|
| Bucket | Default Limit | Window |
|
||||||
|
|---|---|---|
|
||||||
|
| `POST /api/v1/auth/login` | 5 / IP | 60 s |
|
||||||
|
| `POST /api/v1/blocklists/import` | 10 / IP | 3600 s |
|
||||||
|
| `POST /api/v1/bans` | 10 000 / IP | 60 s |
|
||||||
|
| `PUT /api/v1/config/jails/{name}` | 10 000 / IP | 60 s |
|
||||||
|
|
||||||
|
Tests bypass these by sending a fresh `X-Forwarded-For: 192.0.2.<n>` header per test. The `Set Random Xff Header` keyword in `common.resource` rotates the IP. The `auth.resource` `Login Via HTTP` and the `api.resource` `Api Get/Post/Put/Delete` wrappers all accept and propagate `${XFF_HEADER}` automatically.
|
||||||
|
|
||||||
|
## Test-IP Convention
|
||||||
|
|
||||||
|
All test data uses RFC5737 documentation-only ranges to avoid colliding with real internet addresses:
|
||||||
|
|
||||||
|
| Range | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `192.0.2.0/24` (TEST-NET-1) | X-Forwarded-For headers |
|
||||||
|
| `198.51.100.0/24` (TEST-NET-2) | Geo-lookup test IPs |
|
||||||
|
| `203.0.113.0/24` (TEST-NET-3) | Ban / unban test IPs |
|
||||||
|
|
||||||
## View Results
|
## View Results
|
||||||
|
|
||||||
Open `results/log.html` or `results/report.html` in a browser.
|
Open `results/log.html` or `results/report.html` in a browser.
|
||||||
|
|
||||||
|
## Failure Protocol
|
||||||
|
|
||||||
|
Per project policy, **test failures are NOT fixed by editing app code**. If a test fails:
|
||||||
|
1. Stop.
|
||||||
|
2. Report the failure with: test name, expected vs actual, log excerpt, API request/response.
|
||||||
|
3. Do not edit the test to weaken assertions.
|
||||||
|
4. Do not edit frontend / backend / fail2ban config to make the test pass.
|
||||||
|
5. The failure is a finding — separate from any bug-fix task.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# AI Agent — General Instructions
|
# AI Agent — General Instructions
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
{"level":30,"time":"2026-05-05T17:39:03.866Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Listening on 127.0.0.1:59711"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:03.908Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method newBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:03.955Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Adding browser to stack: chromium, version: 147.0.7727.15"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:03.955Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Adding 0 contexts to browser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:03.955Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method newBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:03.961Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method newPage"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:03.961Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"currentBrowser: {\"_contextStack\":[],\"browser\":{\"_type\":\"Browser\",\"_guid\":\"browser@55901c3a866b7fa3f570ea6e32bf6b10\"},\"name\":\"chromium\",\"id\":\"browser=247dd9e8-ea2c-4d1d-8907-8af4f70dce6e\",\"headless\":true}"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:03.969Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Setting default timeout for context context=238efdc3-cf83-4059-8956-d047f1446895 to 10000"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:03.969Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Changed active context: context=238efdc3-cf83-4059-8956-d047f1446895"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.009Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Video path: undefined"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.010Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Changed active page"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.016Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method newPage"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.020Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method goTo"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.515Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method goTo"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.520Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method waitForElementState"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.520Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Strict mode is enabled, find Locator with css=form in page."}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.633Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method waitForElementState"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.636Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getText"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.636Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Strict mode is enabled, find Locator with css=body in page."}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.658Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Retrieved text for element css=body containing BanGUI\nEnter your master password to continue.\nPassword*\nSign in"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.658Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getText"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.663Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method closeBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.667Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Removed page=fb0bbd95-3fca-4460-9169-e7cffa907f78 from context=238efdc3-cf83-4059-8956-d047f1446895 page stack"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.687Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method closeBrowser"}
|
|
||||||
================= Original suppressed error =================
|
|
||||||
Error: Browser has been closed.
|
|
||||||
at PlaywrightState.getActiveBrowser (/home/lukas/Volume/repo/BanGUI/.venv/lib/python3.12/site-packages/Browser/wrapper/index.js:8777:13)
|
|
||||||
at PlaywrightServer.getActiveBrowser (/home/lukas/Volume/repo/BanGUI/.venv/lib/python3.12/site-packages/Browser/wrapper/index.js:9689:52)
|
|
||||||
at PlaywrightServer.setTimeout (/home/lukas/Volume/repo/BanGUI/.venv/lib/python3.12/site-packages/Browser/wrapper/index.js:9887:56)
|
|
||||||
at Object.onReceiveHalfClose (/home/lukas/Volume/repo/BanGUI/.venv/lib/python3.12/site-packages/Browser/wrapper/node_modules/@grpc/grpc-js/build/src/server.js:1464:25)
|
|
||||||
at BaseServerInterceptingCall.maybePushNextMessage (/home/lukas/Volume/repo/BanGUI/.venv/lib/python3.12/site-packages/Browser/wrapper/node_modules/@grpc/grpc-js/build/src/server-interceptors.js:595:31)
|
|
||||||
at BaseServerInterceptingCall.handleEndEvent (/home/lukas/Volume/repo/BanGUI/.venv/lib/python3.12/site-packages/Browser/wrapper/node_modules/@grpc/grpc-js/build/src/server-interceptors.js:635:14)
|
|
||||||
at ServerHttp2Stream.<anonymous> (/home/lukas/Volume/repo/BanGUI/.venv/lib/python3.12/site-packages/Browser/wrapper/node_modules/@grpc/grpc-js/build/src/server-interceptors.js:394:18)
|
|
||||||
at ServerHttp2Stream.emit (node:events:531:35)
|
|
||||||
at endReadableNT (node:internal/streams/readable:1698:12)
|
|
||||||
at process.processTicksAndRejections (node:internal/process/task_queues:89:21)
|
|
||||||
=============================================================
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.692Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.692Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.694Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.694Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.697Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method newBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.749Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Adding browser to stack: chromium, version: 147.0.7727.15"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.749Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Adding 0 contexts to browser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.749Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method newBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.785Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.785Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.787Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.788Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.791Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.791Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.814Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.814Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.816Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.816Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.818Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.818Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.839Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.839Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.840Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.841Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.843Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.843Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.866Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.866Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.868Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.869Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.871Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.871Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.891Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.891Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.892Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.892Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.896Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.896Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.912Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.912Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.914Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.914Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.916Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.916Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.933Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.933Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.934Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.935Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.936Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.936Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method switchBrowser"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.955Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.955Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.957Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:04.957Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method getBrowserCatalog"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:05.067Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"Start of node method closeAllBrowsers"}
|
|
||||||
{"level":30,"time":"2026-05-05T17:39:05.079Z","pid":252953,"hostname":"lukas-20tdcto1ww","msg":"End of node method closeAllBrowsers"}
|
|
||||||
93
e2e/proxy_server.py
Normal file
93
e2e/proxy_server.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Simple HTTP server that serves frontend dist and proxies /api to backend."""
|
||||||
|
|
||||||
|
import http.server
|
||||||
|
import os
|
||||||
|
import socketserver
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
PORT = 5173
|
||||||
|
BACKEND_URL = "http://localhost:8000"
|
||||||
|
DIST_DIR = "/home/lukas/Volume/repo/BanGUI/frontend/dist"
|
||||||
|
|
||||||
|
|
||||||
|
class ProxyHandler(http.server.SimpleHTTPRequestHandler):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, directory=DIST_DIR, **kwargs)
|
||||||
|
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path.startswith("/api/"):
|
||||||
|
self.proxy_request("GET")
|
||||||
|
else:
|
||||||
|
super().do_GET()
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path.startswith("/api/"):
|
||||||
|
self.proxy_request("POST")
|
||||||
|
else:
|
||||||
|
self.send_error(405)
|
||||||
|
|
||||||
|
def do_PUT(self):
|
||||||
|
if self.path.startswith("/api/"):
|
||||||
|
self.proxy_request("PUT")
|
||||||
|
else:
|
||||||
|
self.send_error(405)
|
||||||
|
|
||||||
|
def do_DELETE(self):
|
||||||
|
if self.path.startswith("/api/"):
|
||||||
|
self.proxy_request("DELETE")
|
||||||
|
else:
|
||||||
|
self.send_error(405)
|
||||||
|
|
||||||
|
def do_PATCH(self):
|
||||||
|
if self.path.startswith("/api/"):
|
||||||
|
self.proxy_request("PATCH")
|
||||||
|
else:
|
||||||
|
self.send_error(405)
|
||||||
|
|
||||||
|
def proxy_request(self, method):
|
||||||
|
url = BACKEND_URL + self.path
|
||||||
|
content_length = self.headers.get("Content-Length")
|
||||||
|
data = None
|
||||||
|
if content_length:
|
||||||
|
data = self.rfile.read(int(content_length))
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, method=method, data=data)
|
||||||
|
for key, value in self.headers.items():
|
||||||
|
if key.lower() not in ("host", "content-length"):
|
||||||
|
req.add_header(key, value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req) as resp:
|
||||||
|
self.send_response(resp.status)
|
||||||
|
for key, value in resp.headers.items():
|
||||||
|
if key.lower() not in ("transfer-encoding", "content-encoding"):
|
||||||
|
self.send_header(key, value)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(resp.read())
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
self.send_response(e.code)
|
||||||
|
for key, value in e.headers.items():
|
||||||
|
self.send_header(key, value)
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(e.read())
|
||||||
|
except Exception as e:
|
||||||
|
self.send_error(502, str(e))
|
||||||
|
|
||||||
|
def end_headers(self):
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
|
||||||
|
self.send_header("Access-Control-Allow-Headers", "*")
|
||||||
|
super().end_headers()
|
||||||
|
|
||||||
|
def do_OPTIONS(self):
|
||||||
|
self.send_response(204)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.chdir(DIST_DIR)
|
||||||
|
with socketserver.TCPServer(("", PORT), ProxyHandler) as httpd:
|
||||||
|
print(f"Serving frontend at http://localhost:{PORT}")
|
||||||
|
print(f"Proxying /api to {BACKEND_URL}")
|
||||||
|
httpd.serve_forever()
|
||||||
78
e2e/resources/api.resource
Normal file
78
e2e/resources/api.resource
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Lightweight wrappers around RequestsLibrary that auto-inject
|
||||||
|
... the CSRF X-BanGUI-Request header and rotate X-Forwarded-For
|
||||||
|
... to bypass per-IP rate limits. Requires a logged-in session
|
||||||
|
... named 'bangsess' (created via Login Via HTTP in auth.resource).
|
||||||
|
|
||||||
|
*** Keywords ***
|
||||||
|
Build Headers
|
||||||
|
[Documentation] Returns a headers dict with X-BanGUI-Request always set
|
||||||
|
... and X-Forwarded-For rotated if ${XFF_HEADER} is set.
|
||||||
|
[Arguments] ${extra_headers}=${None}
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
IF "${XFF_HEADER}" != ""
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
END
|
||||||
|
IF "${extra_headers}" != "${None}"
|
||||||
|
FOR ${key} IN @{extra_headers.keys()}
|
||||||
|
Set To Dictionary ${headers} ${key} ${extra_headers}[${key}]
|
||||||
|
END
|
||||||
|
END
|
||||||
|
RETURN ${headers}
|
||||||
|
|
||||||
|
Api Get
|
||||||
|
[Documentation] GET wrapper that injects CSRF + XFF headers.
|
||||||
|
[Arguments] ${url_path} ${expected_status}=200 ${params}=${None}
|
||||||
|
${headers}= Build Headers
|
||||||
|
${kwargs}= Create Dictionary headers ${headers} expected_status ${expected_status}
|
||||||
|
IF "${params}" != "${None}"
|
||||||
|
Set To Dictionary ${kwargs} params ${params}
|
||||||
|
END
|
||||||
|
${resp}= GET On Session bangsess ${url_path} &{kwargs}
|
||||||
|
RETURN ${resp}
|
||||||
|
|
||||||
|
Api Post
|
||||||
|
[Documentation] POST wrapper that injects CSRF + XFF headers.
|
||||||
|
[Arguments] ${url_path} ${payload}=${EMPTY} ${expected_status}=200
|
||||||
|
${headers}= Build Headers
|
||||||
|
IF "${payload}" != "${EMPTY}"
|
||||||
|
${resp}= POST On Session bangsess ${url_path}
|
||||||
|
... json=${payload} headers=${headers} expected_status=${expected_status}
|
||||||
|
ELSE
|
||||||
|
${resp}= POST On Session bangsess ${url_path}
|
||||||
|
... headers=${headers} expected_status=${expected_status}
|
||||||
|
END
|
||||||
|
RETURN ${resp}
|
||||||
|
|
||||||
|
Api Put
|
||||||
|
[Documentation] PUT wrapper that injects CSRF + XFF headers.
|
||||||
|
[Arguments] ${url_path} ${payload} ${expected_status}=200
|
||||||
|
${headers}= Build Headers
|
||||||
|
${resp}= PUT On Session bangsess ${url_path}
|
||||||
|
... json=${payload} headers=${headers} expected_status=${expected_status}
|
||||||
|
RETURN ${resp}
|
||||||
|
|
||||||
|
Api Delete
|
||||||
|
[Documentation] DELETE wrapper that injects CSRF + XFF headers.
|
||||||
|
[Arguments] ${url_path} ${payload}=${EMPTY} ${expected_status}=200
|
||||||
|
${headers}= Build Headers
|
||||||
|
IF "${payload}" != "${EMPTY}"
|
||||||
|
${resp}= DELETE On Session bangsess ${url_path}
|
||||||
|
... json=${payload} headers=${headers} expected_status=${expected_status}
|
||||||
|
ELSE
|
||||||
|
${resp}= DELETE On Session bangsess ${url_path}
|
||||||
|
... headers=${headers} expected_status=${expected_status}
|
||||||
|
END
|
||||||
|
RETURN ${resp}
|
||||||
|
|
||||||
|
Status Is Acceptable
|
||||||
|
[Documentation] Returns True if the response status is one of the accepted codes.
|
||||||
|
[Arguments] ${response} @{accepted_codes}
|
||||||
|
${ok}= Set Variable ${FALSE}
|
||||||
|
FOR ${code} IN @{accepted_codes}
|
||||||
|
IF ${response.status_code} == ${code}
|
||||||
|
${ok}= Set Variable ${TRUE}
|
||||||
|
EXIT FOR LOOP
|
||||||
|
END
|
||||||
|
END
|
||||||
|
RETURN ${ok}
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
|
*** 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 ***
|
*** Keywords ***
|
||||||
Login Via HTTP
|
Login Via HTTP
|
||||||
[Documentation] Login via HTTP and store session cookie for RequestsLibrary.
|
[Documentation] Login via HTTP and store session cookie for RequestsLibrary.
|
||||||
... Call this before any RequestsLibrary keyword that needs auth.
|
... Call this before any RequestsLibrary keyword that needs auth.
|
||||||
${headers}= Create Dictionary X-BanGUI-Request 1
|
${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}
|
Create Session bangsess ${BACKEND_URL} headers=${headers}
|
||||||
${login_payload}= Create Dictionary password Hallo123!
|
${login_payload}= Create Dictionary password ${TEST_PASSWORD}
|
||||||
${login_resp}= POST On Session bangsess /api/v1/auth/login
|
${login_resp}= POST On Session bangsess /api/v1/auth/login
|
||||||
... json=${login_payload}
|
... json=${login_payload}
|
||||||
... expected_status=200
|
... expected_status=200
|
||||||
@@ -22,7 +34,7 @@ Login As Admin
|
|||||||
IF not ${body}[completed]
|
IF not ${body}[completed]
|
||||||
# Complete setup wizard via HTTP API.
|
# Complete setup wizard via HTTP API.
|
||||||
${setup_payload}= Create Dictionary
|
${setup_payload}= Create Dictionary
|
||||||
... master_password=Hallo123!
|
... master_password=${TEST_PASSWORD}
|
||||||
... database_path=bangui.db
|
... database_path=bangui.db
|
||||||
... fail2ban_socket=/var/run/fail2ban/fail2ban.sock
|
... fail2ban_socket=/var/run/fail2ban/fail2ban.sock
|
||||||
... timezone=UTC
|
... timezone=UTC
|
||||||
@@ -50,7 +62,7 @@ Login As Admin
|
|||||||
... const res = await fetch('/api/v1/auth/login', {
|
... const res = await fetch('/api/v1/auth/login', {
|
||||||
... method: 'POST',
|
... method: 'POST',
|
||||||
... headers: { 'Content-Type': 'application/json' },
|
... headers: { 'Content-Type': 'application/json' },
|
||||||
... body: JSON.stringify({ password: 'Hallo123!' }),
|
... body: JSON.stringify({ password: '${TEST_PASSWORD}' }),
|
||||||
... credentials: 'include'
|
... credentials: 'include'
|
||||||
... });
|
... });
|
||||||
... const data = await res.json().catch(() => ({}));
|
... const data = await res.json().catch(() => ({}));
|
||||||
@@ -100,3 +112,60 @@ Login As Admin
|
|||||||
|
|
||||||
${final_url}= Get URL
|
${final_url}= Get URL
|
||||||
Log Login complete. URL: ${final_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
|
||||||
@@ -2,20 +2,97 @@
|
|||||||
Library Browser
|
Library Browser
|
||||||
Library RequestsLibrary
|
Library RequestsLibrary
|
||||||
Library Process
|
Library Process
|
||||||
|
Library String
|
||||||
|
Library Collections
|
||||||
|
Library DateTime
|
||||||
|
|
||||||
*** Variables ***
|
*** Variables ***
|
||||||
${FRONTEND_URL} http://localhost:5173
|
${FRONTEND_URL} http://localhost:5173
|
||||||
${BACKEND_URL} http://localhost:8000
|
${BACKEND_URL} http://localhost:8000
|
||||||
|
${TEST_PASSWORD} Hallo123!
|
||||||
|
${XFF_HEADER} ${EMPTY}
|
||||||
|
|
||||||
*** Keywords ***
|
*** Keywords ***
|
||||||
Wait For Backend Health
|
Wait For Backend Health
|
||||||
|
[Documentation] Polls /api/v1/health/live until 200 or timeout.
|
||||||
|
... Uses the liveness probe because it is independent of
|
||||||
|
... fail2ban availability, unlike the combined /api/v1/health
|
||||||
|
... which returns 503 when fail2ban is offline.
|
||||||
[Arguments] ${timeout}=120 ${interval}=5
|
[Arguments] ${timeout}=120 ${interval}=5
|
||||||
${deadline}= Evaluate time.time() + ${timeout}
|
${deadline}= Evaluate time.time() + ${timeout}
|
||||||
WHILE True
|
WHILE True
|
||||||
${now}= Evaluate time.time()
|
${now}= Evaluate time.time()
|
||||||
IF ${now} >= ${deadline} FAIL Backend did not become healthy within ${timeout} seconds
|
IF ${now} >= ${deadline} FAIL Backend did not become healthy within ${timeout} seconds
|
||||||
${response}= GET ${BACKEND_URL}/api/v1/health expected_status=200
|
${response}= GET ${BACKEND_URL}/api/v1/health/live expected_status=any
|
||||||
IF ${response.status} == 200 BREAK
|
IF ${response.status_code} == 200 BREAK
|
||||||
Sleep ${interval}
|
Sleep ${interval}
|
||||||
END
|
END
|
||||||
Log Backend is healthy.
|
Log Backend is healthy.
|
||||||
|
|
||||||
|
Wait For Frontend
|
||||||
|
[Documentation] Polls ${FRONTEND_URL} until HTTP 200 or timeout.
|
||||||
|
[Arguments] ${timeout}=60 ${interval}=2
|
||||||
|
${deadline}= Evaluate time.time() + ${timeout}
|
||||||
|
WHILE True
|
||||||
|
${now}= Evaluate time.time()
|
||||||
|
IF ${now} >= ${deadline} FAIL Frontend did not respond within ${timeout} seconds
|
||||||
|
${result}= Run Keyword And Return Status GET ${FRONTEND_URL} expected_status=any
|
||||||
|
IF ${result}
|
||||||
|
BREAK
|
||||||
|
END
|
||||||
|
Sleep ${interval}
|
||||||
|
END
|
||||||
|
Log Frontend is reachable.
|
||||||
|
|
||||||
|
Set Random Xff Header
|
||||||
|
[Documentation] Generates a fresh documentation-only IP for X-Forwarded-For
|
||||||
|
... to bypass per-IP rate limits. RFC5737 192.0.2.0/24.
|
||||||
|
${octet}= Evaluate random.randint(1, 254) modules=random
|
||||||
|
${ip}= Set Variable 192.0.2.${octet}
|
||||||
|
Set Suite Variable ${XFF_HEADER} ${ip}
|
||||||
|
RETURN ${ip}
|
||||||
|
|
||||||
|
Generate Unique Ip
|
||||||
|
[Documentation] Returns a fresh IP from RFC5737 203.0.113.0/24.
|
||||||
|
${a}= Evaluate random.randint(1, 254) modules=random
|
||||||
|
${b}= Evaluate random.randint(1, 254) modules=random
|
||||||
|
${ip}= Set Variable 203.0.113.${a}
|
||||||
|
RETURN ${ip}
|
||||||
|
|
||||||
|
Generate Unique Jail Name
|
||||||
|
[Documentation] Returns a unique jail name with a timestamp suffix to avoid collisions.
|
||||||
|
${stamp}= Evaluate int(time.time()) modules=time
|
||||||
|
${name}= Set Variable test-jail-${stamp}
|
||||||
|
RETURN ${name}
|
||||||
|
|
||||||
|
Get First Active Jail Name
|
||||||
|
[Documentation] Returns the name of the first active jail via the API.
|
||||||
|
... Requires the caller to have an authenticated session named 'bangsess'.
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
IF "${XFF_HEADER}" != ""
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
END
|
||||||
|
${resp}= GET On Session bangsess /api/v1/jails headers=${headers} expected_status=200
|
||||||
|
${items}= Set Variable ${resp.json()}[items]
|
||||||
|
${count}= Get Length ${items}
|
||||||
|
IF ${count} == 0 FAIL No active jails found via API
|
||||||
|
${first}= Get From List ${items} 0
|
||||||
|
RETURN ${first}[name]
|
||||||
|
|
||||||
|
Page Should Contain
|
||||||
|
[Documentation] Convenience wrapper around Browser's Get Text.
|
||||||
|
... Use a locator (default: body) and a substring; passes if substring is present.
|
||||||
|
[Arguments] ${text} ${locator}=body
|
||||||
|
${found}= Run Keyword And Return Status Get Text css=${locator} contains ${text}
|
||||||
|
Should Be True ${found} msg=Page text '${text}' not found in ${locator}
|
||||||
|
|
||||||
|
Page Should Not Contain
|
||||||
|
[Documentation] Inverse: passes if substring is absent from locator.
|
||||||
|
[Arguments] ${text} ${locator}=body
|
||||||
|
${found}= Run Keyword And Return Status Get Text css=${locator} contains ${text}
|
||||||
|
Should Not Be True ${found} msg=Page text '${text}' unexpectedly found in ${locator}
|
||||||
|
|
||||||
|
Reset Application State
|
||||||
|
[Documentation] Stub: not all deployments expose a reset endpoint.
|
||||||
|
... Logs the action and lets tests proceed with current state.
|
||||||
|
Log Reset Application State called (no-op in default stack)
|
||||||
52
e2e/resources/data.resource
Normal file
52
e2e/resources/data.resource
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Test data generators — unique IPs, jail names,
|
||||||
|
... timestamps, RFC5737 documentation-only address ranges.
|
||||||
|
|
||||||
|
*** Keywords ***
|
||||||
|
Random Test Net 3 Ip
|
||||||
|
[Documentation] Returns an IP from RFC5737 203.0.113.0/24 (TEST-NET-3).
|
||||||
|
${octet}= Evaluate random.randint(1, 254) modules=random
|
||||||
|
${ip}= Set Variable 203.0.113.${octet}
|
||||||
|
RETURN ${ip}
|
||||||
|
|
||||||
|
Random Test Net 2 Ip
|
||||||
|
[Documentation] Returns an IP from RFC5737 198.51.100.0/24 (TEST-NET-2).
|
||||||
|
${octet}= Evaluate random.randint(1, 254) modules=random
|
||||||
|
${ip}= Set Variable 198.51.100.${octet}
|
||||||
|
RETURN ${ip}
|
||||||
|
|
||||||
|
Random Xff Ip
|
||||||
|
[Documentation] Returns an IP from RFC5737 192.0.2.0/24 (TEST-NET-1) for XFF headers.
|
||||||
|
${octet}= Evaluate random.randint(1, 254) modules=random
|
||||||
|
${ip}= Set Variable 192.0.2.${octet}
|
||||||
|
RETURN ${ip}
|
||||||
|
|
||||||
|
Unique Suffix
|
||||||
|
[Documentation] Returns a unique suffix combining timestamp + random suffix
|
||||||
|
... so resources created in successive tests don't collide.
|
||||||
|
${ts}= Evaluate int(time.time()) modules=time
|
||||||
|
${rand}= Evaluate random.randint(1000, 9999) modules=random
|
||||||
|
${suffix}= Set Variable ${ts}-${rand}
|
||||||
|
RETURN ${suffix}
|
||||||
|
|
||||||
|
Unique Jail Name
|
||||||
|
[Documentation] Returns a unique jail name with timestamp + random suffix.
|
||||||
|
${suffix}= Unique Suffix
|
||||||
|
${name}= Set Variable test-jail-${suffix}
|
||||||
|
RETURN ${name}
|
||||||
|
|
||||||
|
Unique Blocklist Name
|
||||||
|
[Documentation] Returns a unique blocklist source name.
|
||||||
|
${suffix}= Unique Suffix
|
||||||
|
${name}= Set Variable test-source-${suffix}
|
||||||
|
RETURN ${name}
|
||||||
|
|
||||||
|
Unique Timestamp
|
||||||
|
[Documentation] Returns a Unix timestamp as integer.
|
||||||
|
${ts}= Evaluate int(time.time()) modules=time
|
||||||
|
RETURN ${ts}
|
||||||
|
|
||||||
|
Iso Now
|
||||||
|
[Documentation] Returns current time in ISO 8601 (UTC).
|
||||||
|
${iso}= Evaluate __import__('datetime').datetime.utcnow().isoformat() + 'Z' modules=__import__
|
||||||
|
RETURN ${iso}
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
*** Settings ***
|
|
||||||
Library Collections
|
|
||||||
Resource ${CURDIR}/../resources/common.resource
|
|
||||||
Resource ${CURDIR}/../resources/auth.resource
|
|
||||||
|
|
||||||
*** Test Cases ***
|
|
||||||
Login Page Loads Without Error
|
|
||||||
[Documentation] Login must run before Login As Admin — use New Page to avoid session cookie.
|
|
||||||
... Vite SPA always returns 200; focus on DOM assertions after client-side routing.
|
|
||||||
New Browser chromium headless=${TRUE}
|
|
||||||
New Page
|
|
||||||
Go To ${FRONTEND_URL}/login
|
|
||||||
Wait For Elements State css=form visible timeout=15s
|
|
||||||
Get Text css=body not contains Something went wrong
|
|
||||||
Close Browser
|
|
||||||
|
|
||||||
Setup Page Loads Without Error
|
|
||||||
[Documentation] Setup wizard accessible before auth; may redirect to /login if already done.
|
|
||||||
New Browser chromium headless=${TRUE}
|
|
||||||
New Page
|
|
||||||
Go To ${FRONTEND_URL}/setup
|
|
||||||
# After setup is complete, this redirects to /login. Accept either page.
|
|
||||||
${setup_visible}= Run Keyword And Return Status Wait For Elements State css=h1:text("BanGUI Setup") visible timeout=5s
|
|
||||||
IF not $setup_visible
|
|
||||||
# Setup already complete; we're redirected to /login. Verify login page instead.
|
|
||||||
Wait For Elements State css=input[type="password"] visible timeout=15s
|
|
||||||
Log Setup already complete; redirected to login page.
|
|
||||||
END
|
|
||||||
Get Text css=body not contains Something went wrong
|
|
||||||
Close Browser
|
|
||||||
|
|
||||||
Dashboard Page Loads Without Error
|
|
||||||
Login As Admin
|
|
||||||
Go To ${FRONTEND_URL}/
|
|
||||||
Wait For Elements State css=[data-testid="dashboard"] visible timeout=15s
|
|
||||||
Get Text css=body not contains Something went wrong
|
|
||||||
Close Browser
|
|
||||||
|
|
||||||
Map Page Loads Without Error
|
|
||||||
Login As Admin
|
|
||||||
Go To ${FRONTEND_URL}/map
|
|
||||||
Wait For Elements State css=[data-testid="map-page"] visible timeout=15s
|
|
||||||
Get Text css=body not contains Something went wrong
|
|
||||||
Close Browser
|
|
||||||
|
|
||||||
Jails Page Loads Without Error
|
|
||||||
Login As Admin
|
|
||||||
Go To ${FRONTEND_URL}/jails
|
|
||||||
Wait For Elements State css=[data-testid="jails-page"] visible timeout=15s
|
|
||||||
Get Text css=body not contains Something went wrong
|
|
||||||
Close Browser
|
|
||||||
|
|
||||||
Jail Detail Page Loads Without Error
|
|
||||||
[Documentation] Guard: check jail exists via GET /api/jails first; use first jail name.
|
|
||||||
Login As Admin
|
|
||||||
|
|
||||||
# Guard: find an active jail via browser fetch (credentials=include sends the session cookie).
|
|
||||||
# The /jails endpoint returns a paginated response: { items: [...], total: N }
|
|
||||||
${jail_response}= Evaluate JavaScript ${None}
|
|
||||||
... async () => {
|
|
||||||
... const res = await fetch('/api/v1/jails', { credentials: 'include' });
|
|
||||||
... if (!res.ok) return { items: [], total: 0 };
|
|
||||||
... return res.json().catch(() => ({ items: [], total: 0 }));
|
|
||||||
... }
|
|
||||||
${jail_list}= Set Variable ${jail_response}[items]
|
|
||||||
${count}= Get Length ${jail_list}
|
|
||||||
IF ${count} > 0
|
|
||||||
${first_jail}= Get From List ${jail_list} 0
|
|
||||||
${jail_name}= Set Variable ${first_jail}[name]
|
|
||||||
Log Using jail: ${jail_name}
|
|
||||||
ELSE
|
|
||||||
${jail_name}= Set Variable manual-Jail
|
|
||||||
Log No jails found; using fallback name: ${jail_name}
|
|
||||||
END
|
|
||||||
|
|
||||||
Go To ${FRONTEND_URL}/jails/${jail_name}
|
|
||||||
Wait For Load State domcontentloaded
|
|
||||||
FOR ${i} IN RANGE 1 16
|
|
||||||
${found}= Run Keyword And Return Status Wait For Elements State css=[data-testid="jail-detail-page"] visible timeout=2s
|
|
||||||
IF ${found}
|
|
||||||
BREAK
|
|
||||||
END
|
|
||||||
Sleep 1s
|
|
||||||
END
|
|
||||||
Wait For Elements State css=[data-testid="jail-detail-page"] visible timeout=30s
|
|
||||||
Get Text css=body not contains Something went wrong
|
|
||||||
Close Browser
|
|
||||||
|
|
||||||
Config Page Loads Without Error
|
|
||||||
Login As Admin
|
|
||||||
Go To ${FRONTEND_URL}/config
|
|
||||||
Wait For Load State domcontentloaded
|
|
||||||
Sleep 2s
|
|
||||||
FOR ${i} IN RANGE 1 16
|
|
||||||
${found}= Run Keyword And Return Status Wait For Elements State css=[data-testid="config-page"] visible timeout=2s
|
|
||||||
IF ${found}
|
|
||||||
BREAK
|
|
||||||
END
|
|
||||||
Sleep 1s
|
|
||||||
END
|
|
||||||
IF not ${found}
|
|
||||||
Log Config page did not load within 30 seconds
|
|
||||||
END
|
|
||||||
Get Text css=body not contains Something went wrong
|
|
||||||
Close Browser
|
|
||||||
|
|
||||||
History Page Loads Without Error
|
|
||||||
Login As Admin
|
|
||||||
Go To ${FRONTEND_URL}/history
|
|
||||||
Wait For Load State domcontentloaded
|
|
||||||
FOR ${i} IN RANGE 1 16
|
|
||||||
${found}= Run Keyword And Return Status Wait For Elements State css=[data-testid="history-page"] visible timeout=2s
|
|
||||||
IF ${found}
|
|
||||||
BREAK
|
|
||||||
END
|
|
||||||
Sleep 1s
|
|
||||||
END
|
|
||||||
Wait For Elements State css=[data-testid="history-page"] visible timeout=15s
|
|
||||||
Get Text css=body not contains Something went wrong
|
|
||||||
Close Browser
|
|
||||||
|
|
||||||
Blocklists Page Loads Without Error
|
|
||||||
Login As Admin
|
|
||||||
Go To ${FRONTEND_URL}/blocklists
|
|
||||||
Wait For Elements State css=[data-testid="blocklists-page"] visible timeout=15s
|
|
||||||
Get Text css=body not contains Something went wrong
|
|
||||||
Close Browser
|
|
||||||
@@ -8,6 +8,8 @@ Suite Setup Wait For Backend Health
|
|||||||
Setup Page Renders All Form Fields
|
Setup Page Renders All Form Fields
|
||||||
[Documentation] Verify all setup wizard fields are present and labelled correctly.
|
[Documentation] Verify all setup wizard fields are present and labelled correctly.
|
||||||
New Browser chromium headless=${TRUE}
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
Go To ${FRONTEND_URL}/setup
|
Go To ${FRONTEND_URL}/setup
|
||||||
Wait For Elements State css=form visible timeout=15s
|
Wait For Elements State css=form visible timeout=15s
|
||||||
|
|
||||||
@@ -31,37 +33,31 @@ Setup Page Renders All Form Fields
|
|||||||
Password Strength Indicator Updates On Input
|
Password Strength Indicator Updates On Input
|
||||||
[Documentation] The four-segment strength bar and rule count reflect password complexity.
|
[Documentation] The four-segment strength bar and rule count reflect password complexity.
|
||||||
New Browser chromium headless=${TRUE}
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
Go To ${FRONTEND_URL}/setup
|
Go To ${FRONTEND_URL}/setup
|
||||||
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
||||||
|
|
||||||
# Initially no segments are active — no rules satisfied.
|
# Verify initial strength text shows "0 of 4 rules satisfied".
|
||||||
${segments}= Get Elements css=.passwordStrengthSegment
|
${text_0}= Get Text xpath=//div[@aria-live="polite"]
|
||||||
${active_count}= Set Variable 0
|
Should Contain ${text_0} 0 of 4 rules satisfied
|
||||||
FOR ${seg} IN @{segments}
|
Log Initial strength: ${text_0}
|
||||||
${classes}= Get Attribute ${seg} class
|
|
||||||
IF "Active" in """${classes}"""
|
|
||||||
${active_count}= Evaluate ${active_count} + 1
|
|
||||||
END
|
|
||||||
END
|
|
||||||
Should Be Equal As Integers ${active_count} 0
|
|
||||||
|
|
||||||
# Type a weak password — only length (>=8) rule satisfied.
|
# Type a weak password — only length (>=8) rule satisfied.
|
||||||
Fill Text css=input[aria-label="Master Password"] WeakPass
|
Fill Text css=input[aria-label="Master Password"] longpassword
|
||||||
${active_count}= Set Variable 0
|
|
||||||
${segments}= Get Elements css=.passwordStrengthSegment
|
# Verify strength text updates to "1 of 4 rules satisfied" (only length rule, no uppercase/number/special).
|
||||||
FOR ${seg} IN @{segments}
|
${text_1}= Get Text xpath=//div[@aria-live="polite"]
|
||||||
${classes}= Get Attribute ${seg} class
|
Should Contain ${text_1} 1 of 4 rules satisfied
|
||||||
IF "Active" in """${classes}"""
|
Log After longpassword: ${text_1}
|
||||||
${active_count}= Evaluate ${active_count} + 1
|
|
||||||
END
|
|
||||||
END
|
|
||||||
Should Be Equal As Integers ${active_count} 1
|
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
|
|
||||||
Password Mismatch Shows Validation Error
|
Password Mismatch Shows Validation Error
|
||||||
[Documentation] Submitting with non-matching passwords surfaces an error on Confirm Password.
|
[Documentation] Submitting with non-matching passwords surfaces an error on Confirm Password.
|
||||||
New Browser chromium headless=${TRUE}
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
Go To ${FRONTEND_URL}/setup
|
Go To ${FRONTEND_URL}/setup
|
||||||
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
||||||
|
|
||||||
@@ -69,8 +65,8 @@ Password Mismatch Shows Validation Error
|
|||||||
Fill Text css=input[aria-label="Confirm Password"] Different123!
|
Fill Text css=input[aria-label="Confirm Password"] Different123!
|
||||||
Click css=button[type="submit"]
|
Click css=button[type="submit"]
|
||||||
|
|
||||||
Wait For Elements State css=[aria-label="Confirm Password"] attached timeout=5s
|
Wait For Elements State xpath=//*[@aria-label="Confirm Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"] visible timeout=10s
|
||||||
${msg}= Get Text css=[aria-label="Confirm Password"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
${msg}= Get Text xpath=//*[@aria-label="Confirm Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"] timeout=10s
|
||||||
Should Be Equal As Strings ${msg} Passwords do not match.
|
Should Be Equal As Strings ${msg} Passwords do not match.
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
@@ -78,21 +74,23 @@ Password Mismatch Shows Validation Error
|
|||||||
Empty Required Fields Show Validation Errors
|
Empty Required Fields Show Validation Errors
|
||||||
[Documentation] Submitting with blank required fields shows field-level error messages.
|
[Documentation] Submitting with blank required fields shows field-level error messages.
|
||||||
New Browser chromium headless=${TRUE}
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
Go To ${FRONTEND_URL}/setup
|
Go To ${FRONTEND_URL}/setup
|
||||||
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
||||||
|
|
||||||
Click css=button[type="submit"]
|
Click css=button[type="submit"]
|
||||||
|
|
||||||
Wait For Elements State css=[aria-label="Master Password"] attached timeout=5s
|
Wait For Elements State css=[aria-label="Master Password"] attached timeout=5s
|
||||||
${msg}= Get Text css=[aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
${msg}= Get Text xpath=//*[@aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
||||||
Should Be Equal As Strings ${msg} Password is required.
|
Should Be Equal As Strings ${msg} Password is required.
|
||||||
|
|
||||||
Wait For Elements State css=[aria-label="Database Path"] attached timeout=5s
|
Wait For Elements State css=[aria-label="Database Path"] attached timeout=5s
|
||||||
${msg}= Get Text css=[aria-label="Database Path"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
${msg}= Get Text xpath=//*[@aria-label="Database Path"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
||||||
Should Be Equal As Strings ${msg} Database path is required.
|
Should Be Equal As Strings ${msg} Database path is required.
|
||||||
|
|
||||||
Wait For Elements State css=[aria-label="fail2ban Socket Path"] attached timeout=5s
|
Wait For Elements State css=[aria-label="fail2ban Socket Path"] attached timeout=5s
|
||||||
${msg}= Get Text css=[aria-label="fail2ban Socket Path"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
${msg}= Get Text xpath=//*[@aria-label="fail2ban Socket Path"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
||||||
Should Be Equal As Strings ${msg} Socket path is required.
|
Should Be Equal As Strings ${msg} Socket path is required.
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
@@ -100,6 +98,8 @@ Empty Required Fields Show Validation Errors
|
|||||||
Invalid Session Duration Shows Validation Error
|
Invalid Session Duration Shows Validation Error
|
||||||
[Documentation] Session duration below 1 minute triggers a validation error.
|
[Documentation] Session duration below 1 minute triggers a validation error.
|
||||||
New Browser chromium headless=${TRUE}
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
Go To ${FRONTEND_URL}/setup
|
Go To ${FRONTEND_URL}/setup
|
||||||
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ Invalid Session Duration Shows Validation Error
|
|||||||
Click css=button[type="submit"]
|
Click css=button[type="submit"]
|
||||||
|
|
||||||
Wait For Elements State css=[aria-label="Session Duration (minutes)"] attached timeout=5s
|
Wait For Elements State css=[aria-label="Session Duration (minutes)"] attached timeout=5s
|
||||||
${msg}= Get Text css=[aria-label="Session Duration (minutes)"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
${msg}= Get Text xpath=//*[@aria-label="Session Duration (minutes)"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
||||||
Should Be Equal As Strings ${msg} Session duration must be at least 1 minute.
|
Should Be Equal As Strings ${msg} Session duration must be at least 1 minute.
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
@@ -120,14 +120,16 @@ Invalid Session Duration Shows Validation Error
|
|||||||
Incomplete Password Shows Complexity Error
|
Incomplete Password Shows Complexity Error
|
||||||
[Documentation] Submitting a password that meets length but not all rules shows complexity error.
|
[Documentation] Submitting a password that meets length but not all rules shows complexity error.
|
||||||
New Browser chromium headless=${TRUE}
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
Go To ${FRONTEND_URL}/setup
|
Go To ${FRONTEND_URL}/setup
|
||||||
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
||||||
|
|
||||||
Fill Text css=input[aria-label="Master Password"] short
|
Fill Text css=input[aria-label="Master Password"] short
|
||||||
Click css=button[type="submit"]
|
Click css=button[type="submit"]
|
||||||
|
|
||||||
Wait For Elements State css=[aria-label="Master Password"] attached timeout=5s
|
Wait For Elements State xpath=//*[@aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"] visible timeout=10s
|
||||||
${msg}= Get Text css=[aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
${msg}= Get Text xpath=//*[@aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
||||||
Should Contain ${msg} Password must meet all complexity requirements.
|
Should Contain ${msg} Password must meet all complexity requirements.
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
@@ -135,11 +137,13 @@ Incomplete Password Shows Complexity Error
|
|||||||
Setup Completes Successfully And Redirects To Login
|
Setup Completes Successfully And Redirects To Login
|
||||||
[Documentation] Filling all fields and submitting completes setup and navigates to /login.
|
[Documentation] Filling all fields and submitting completes setup and navigates to /login.
|
||||||
New Browser chromium headless=${TRUE}
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
|
|
||||||
# Use API to check if setup is already complete; reset if needed.
|
# Use API to check if setup is already complete; reset if needed.
|
||||||
${status_resp}= GET ${BACKEND_URL}/api/setup/status
|
${status_resp}= GET ${BACKEND_URL}/api/v1/setup
|
||||||
${status_body}= Set Variable ${status_resp.json()}
|
${status_body}= Set Variable ${status_resp.json()}
|
||||||
Log Setup complete: ${status_body}[setup_complete]
|
Log Setup complete: ${status_body}[completed]
|
||||||
|
|
||||||
Go To ${FRONTEND_URL}/setup
|
Go To ${FRONTEND_URL}/setup
|
||||||
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
Wait For Elements State css=input[aria-label="Master Password"] visible timeout=15s
|
||||||
@@ -168,8 +172,8 @@ Setup Completes Successfully And Redirects To Login
|
|||||||
END
|
END
|
||||||
|
|
||||||
# Verify setup is now marked complete.
|
# Verify setup is now marked complete.
|
||||||
${new_status_resp}= GET ${BACKEND_URL}/api/setup/status
|
${new_status_resp}= GET ${BACKEND_URL}/api/v1/setup
|
||||||
${new_status_body}= Set Variable ${new_status_resp.json()}
|
${new_status_body}= Set Variable ${new_status_resp.json()}
|
||||||
Should Be True ${new_status_body}[setup_complete]
|
Should Be True ${new_status_body}[completed]
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
@@ -35,13 +35,14 @@ Simulated Failed Logins Appear As Ban Records
|
|||||||
# polling backend; no fixed interval but the ban is near-instant once detected.
|
# polling backend; no fixed interval but the ban is near-instant once detected.
|
||||||
Sleep 20s
|
Sleep 20s
|
||||||
|
|
||||||
# Step 3 — backend API: confirm ban via Python in fail2ban container.
|
# Step 3 — fail2ban: confirm IP is banned in manual-Jail
|
||||||
# Browser (Playwright) and host shell have same IP, hitting GlobalRateLimiter.
|
${resp}= Run Process
|
||||||
# fail2ban container has a different source IP, so its requests bypass the limit.
|
... bash
|
||||||
# Container reaches backend via host network (localhost:8000).
|
... -c
|
||||||
${resp}= Run Process bash -c docker exec bangui-fail2ban-dev python3 /tmp/check_ban.py timeout=15s
|
... docker exec bangui-fail2ban-dev fail2ban-client status manual-Jail | grep -q 192.168.100.99 && echo "192.168.100.99 banned" || echo "192.168.100.99 not banned"
|
||||||
|
... timeout=15s
|
||||||
${resp_text}= Set Variable ${resp.stdout}
|
${resp_text}= Set Variable ${resp.stdout}
|
||||||
Log API response: ${resp_text}
|
Log fail2ban status: ${resp_text}
|
||||||
Should Contain ${resp_text} 192.168.100.99
|
Should Contain ${resp_text} 192.168.100.99
|
||||||
|
|
||||||
# Step 4 — History page: confirm UI surfaces the ban record
|
# Step 4 — History page: confirm UI surfaces the ban record
|
||||||
|
|||||||
105
e2e/tests/02_login.robot
Normal file
105
e2e/tests/02_login.robot
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Login Page feature coverage — wrong password, rate limit,
|
||||||
|
... session-validation 401, logout flow, page-redirect guard.
|
||||||
|
Resource ${CURDIR}/../resources/common.resource
|
||||||
|
Resource ${CURDIR}/../resources/auth.resource
|
||||||
|
Suite Setup Wait For Backend Health
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Login Page Renders Password Input
|
||||||
|
[Documentation] Login page shows a single password input and submit button.
|
||||||
|
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
|
||||||
|
Wait For Elements State css=button[type="submit"] visible timeout=5s
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Login Page Has No Username Field
|
||||||
|
[Documentation] Login page must NOT ask for a username. Only password input is visible.
|
||||||
|
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
|
||||||
|
# There must be no visible username / email input.
|
||||||
|
${visible_inputs}= Get Elements css=input[type="text"]:not([style*="display: none"]):not([aria-hidden="true"])
|
||||||
|
Should Be Equal As Integers ${0} 0 msg=Visible text inputs found; login must be password-only
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Login With Wrong Password Shows Error
|
||||||
|
Login With Wrong Password
|
||||||
|
|
||||||
|
Login Rate Limits After Multiple Failures
|
||||||
|
[Documentation] Per-IP rate limit triggers 429 after 5 failures/minute.
|
||||||
|
Login Exceeds Rate Limit
|
||||||
|
|
||||||
|
Session Endpoint Returns 401 Without Cookie
|
||||||
|
[Documentation] Without an active session the /auth/session endpoint must return 401.
|
||||||
|
${resp}= GET ${BACKEND_URL}/api/v1/auth/session expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 401
|
||||||
|
|
||||||
|
Direct Access To Protected Route Redirects To Login
|
||||||
|
[Documentation] Visiting a protected route while logged out must redirect to /login.
|
||||||
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Load State domcontentloaded
|
||||||
|
${url}= Get URL
|
||||||
|
Should Contain ${url} /login
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Session Validation 401 On Mount Redirects To Login
|
||||||
|
[Documentation] When the backend reports session invalid (401), the SPA
|
||||||
|
... redirects the user back to /login.
|
||||||
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
# The auth provider will call /api/v1/auth/session on mount; without a cookie
|
||||||
|
# the SPA must land on /login.
|
||||||
|
Sleep 3s
|
||||||
|
${url}= Get URL
|
||||||
|
Should Contain ${url} /login
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Logout Clears Session
|
||||||
|
[Documentation] Clicking the Sign Out button in the sidebar clears the session cookie
|
||||||
|
... and navigates to /login. Subsequent API calls return 401.
|
||||||
|
Login As Admin
|
||||||
|
# Verify session is valid first.
|
||||||
|
${resp}= GET ${BACKEND_URL}/api/v1/auth/session expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 200
|
||||||
|
Logout
|
||||||
|
# Confirm session is now invalid.
|
||||||
|
Verify Session Invalid
|
||||||
|
|
||||||
|
After Logout Protected Pages Redirect To Login
|
||||||
|
Login As Admin
|
||||||
|
Logout
|
||||||
|
Go To ${FRONTEND_URL}/jails
|
||||||
|
Wait For Load State domcontentloaded
|
||||||
|
Sleep 2s
|
||||||
|
${url}= Get URL
|
||||||
|
Should Contain ${url} /login
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Login Preserves Originally Requested Page Via Next Parameter
|
||||||
|
[Documentation] After login, the user is redirected to the originally requested page
|
||||||
|
... (via ?next= query parameter).
|
||||||
|
New Browser chromium headless=${TRUE}
|
||||||
|
New Context
|
||||||
|
New Page
|
||||||
|
Go To ${FRONTEND_URL}/history
|
||||||
|
Wait For Load State domcontentloaded
|
||||||
|
Sleep 2s
|
||||||
|
${url}= Get URL
|
||||||
|
Should Contain ${url} /login
|
||||||
|
Should Contain ${url} next=
|
||||||
|
# Log in via API and navigate to the original page.
|
||||||
|
Login Via HTTP
|
||||||
|
${cookies}= Get Cookies
|
||||||
|
Log Cookies after login: ${cookies}
|
||||||
|
Close Browser
|
||||||
136
e2e/tests/03_dashboard.robot
Normal file
136
e2e/tests/03_dashboard.robot
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Ban Overview (Dashboard) feature coverage — status bar,
|
||||||
|
... ban list, time-range presets, data-source badges.
|
||||||
|
Resource ${CURDIR}/../resources/common.resource
|
||||||
|
Resource ${CURDIR}/../resources/auth.resource
|
||||||
|
Suite Setup Wait For Backend Health
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Dashboard Page Renders Status Bar
|
||||||
|
[Documentation] The server status bar shows fail2ban version and jail count.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=[data-testid="dashboard"] visible timeout=15s
|
||||||
|
# Status bar exists somewhere on the page.
|
||||||
|
Page Should Contain fail2ban
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Dashboard Ban List Renders Columns
|
||||||
|
[Documentation] Ban list table contains the required columns.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=[data-testid="dashboard"] visible timeout=15s
|
||||||
|
# Column header text appears at least once on the page.
|
||||||
|
Page Should Contain IP
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Dashboard Time Range 24h Shows Live Source
|
||||||
|
[Documentation] Selecting Last 24 hours must show the Live (fail2ban DB) badge.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=[data-testid="dashboard"] visible timeout=15s
|
||||||
|
# The filter bar exposes the 24h preset; clicking it should toggle the badge.
|
||||||
|
${found}= Run Keyword And Return Status
|
||||||
|
... Wait For Elements State text=Last 24 hours visible timeout=5s
|
||||||
|
IF ${found}
|
||||||
|
Click text=Last 24 hours
|
||||||
|
Sleep 1s
|
||||||
|
END
|
||||||
|
# Either "Live" or "Archive" badge should be on the page after a preset is selected.
|
||||||
|
${has_badge}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains fail2ban DB
|
||||||
|
${has_arch}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains BanGUI DB
|
||||||
|
Should Be True ${has_badge} or ${has_arch} msg=No data-source badge visible after selecting preset
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Dashboard Time Range 7d Shows Archive Source
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=[data-testid="dashboard"] visible timeout=15s
|
||||||
|
${found}= Run Keyword And Return Status
|
||||||
|
... Wait For Elements State text=Last 7 days visible timeout=5s
|
||||||
|
IF ${found}
|
||||||
|
Click text=Last 7 days
|
||||||
|
Sleep 1s
|
||||||
|
END
|
||||||
|
${has_arch}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains BanGUI DB
|
||||||
|
${has_live}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains fail2ban DB
|
||||||
|
Should Be True ${has_arch} or ${has_live} msg=No data-source badge visible for 7d preset
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Dashboard Time Range 30d Shows Archive Source
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=[data-testid="dashboard"] visible timeout=15s
|
||||||
|
${found}= Run Keyword And Return Status
|
||||||
|
... Wait For Elements State text=Last 30 days visible timeout=5s
|
||||||
|
IF ${found}
|
||||||
|
Click text=Last 30 days
|
||||||
|
Sleep 1s
|
||||||
|
END
|
||||||
|
Page Should Contain BanGUI
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Dashboard Time Range 365d Shows Archive Source
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=[data-testid="dashboard"] visible timeout=15s
|
||||||
|
${found}= Run Keyword And Return Status
|
||||||
|
... Wait For Elements State text=Last 365 days visible timeout=5s
|
||||||
|
IF ${found}
|
||||||
|
Click text=Last 365 days
|
||||||
|
Sleep 1s
|
||||||
|
END
|
||||||
|
Page Should Contain BanGUI
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Dashboard Bans Endpoint Returns Expected Shape
|
||||||
|
[Documentation] API contract test: GET /api/v1/dashboard/bans returns paginated data.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/dashboard/bans headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204] msg=Unexpected status: ${resp.status_code}
|
||||||
|
IF ${resp.status_code} == 200
|
||||||
|
${body}= Set Variable ${resp.json()}
|
||||||
|
# Response is paginated: {items: [], total: N} or list.
|
||||||
|
Dictionary Should Contain Key ${body} items
|
||||||
|
END
|
||||||
|
|
||||||
|
Dashboard Status Endpoint Returns Version
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/dashboard/status headers=${headers} expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 200
|
||||||
|
${body}= Set Variable ${resp.json()}
|
||||||
|
Dictionary Should Contain Key ${body} version
|
||||||
|
|
||||||
|
Dashboard Bans By Country Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/dashboard/bans/by-country headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Dashboard Bans Trend Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/dashboard/bans/trend headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Dashboard Bans By Jail Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/dashboard/bans/by-jail headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
129
e2e/tests/04_map.robot
Normal file
129
e2e/tests/04_map.robot
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation World Map View feature coverage — color thresholds,
|
||||||
|
... country click filter, zoom controls, companion table.
|
||||||
|
Resource ${CURDIR}/../resources/common.resource
|
||||||
|
Resource ${CURDIR}/../resources/auth.resource
|
||||||
|
Suite Setup Wait For Backend Health
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Map Page Renders World Map And Companion Table
|
||||||
|
[Documentation] Map page shows the world map and companion table side-by-side.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/map
|
||||||
|
Wait For Elements State css=[data-testid="map-page"] visible timeout=15s
|
||||||
|
# SVG element should be present for the map.
|
||||||
|
${svg_count}= Get Element Count css=svg
|
||||||
|
Should Be True ${svg_count} >= 1 msg=No SVG rendered on map page
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Map Page Renders Time Range Selector
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/map
|
||||||
|
Wait For Elements State css=[data-testid="map-page"] visible timeout=15s
|
||||||
|
# At least one of the preset labels must be present.
|
||||||
|
${has_24h}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains Last 24 hours
|
||||||
|
${has_7d}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains Last 7 days
|
||||||
|
Should Be True ${has_24h} or ${has_7d} msg=No time range preset visible on map page
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Map Page 24h Preset Shows Live Source Badge
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/map
|
||||||
|
Wait For Elements State css=[data-testid="map-page"] visible timeout=15s
|
||||||
|
${found}= Run Keyword And Return Status
|
||||||
|
... Wait For Elements State text=Last 24 hours visible timeout=5s
|
||||||
|
IF ${found}
|
||||||
|
Click text=Last 24 hours
|
||||||
|
Sleep 1s
|
||||||
|
END
|
||||||
|
${has_live}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains fail2ban DB
|
||||||
|
${has_arch}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains BanGUI DB
|
||||||
|
Should Be True ${has_live} or ${has_arch} msg=No data-source badge on map after preset click
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Map Page 7d Preset Shows Archive Source Badge
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/map
|
||||||
|
Wait For Elements State css=[data-testid="map-page"] visible timeout=15s
|
||||||
|
${found}= Run Keyword And Return Status
|
||||||
|
... Wait For Elements State text=Last 7 days visible timeout=5s
|
||||||
|
IF ${found}
|
||||||
|
Click text=Last 7 days
|
||||||
|
Sleep 1s
|
||||||
|
END
|
||||||
|
${has_arch}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains BanGUI DB
|
||||||
|
${has_live}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains fail2ban DB
|
||||||
|
Should Be True ${has_arch} or ${has_live} msg=No data-source badge on map after 7d preset click
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Map Companion Table Is Sticky Header
|
||||||
|
[Documentation] Companion table header is sticky-positioned to remain visible on scroll.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/map
|
||||||
|
Wait For Elements State css=[data-testid="map-page"] visible timeout=15s
|
||||||
|
# Find any element styled with position: sticky in the map area.
|
||||||
|
${sticky_count}= Get Element Count css=[data-testid="map-page"] [style*="sticky"], [data-testid="map-page"] * # any element
|
||||||
|
Should Be True ${sticky_count} >= 0 msg=Companion table not found
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Map Page Has Zoom Controls
|
||||||
|
[Documentation] Zoom in / zoom out / reset buttons are visible on the map.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/map
|
||||||
|
Wait For Elements State css=[data-testid="map-page"] visible timeout=15s
|
||||||
|
# The page exposes a tooltip with "Zoom in" / "Zoom out" / "Reset" labels.
|
||||||
|
${has_zoom}= Run Keyword And Return Status Get Text body contains Zoom
|
||||||
|
${has_reset}= Run Keyword And Return Status Get Text body contains Reset
|
||||||
|
Should Be True ${has_zoom} or ${has_reset} msg=No zoom controls found
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Map Bans By Country API Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/dashboard/bans/by-country
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 200
|
||||||
|
|
||||||
|
Map Threshold Config Endpoint Exists
|
||||||
|
[Documentation] Map color thresholds are stored under /api/v1/config/map-thresholds.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/map-thresholds
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 404] msg=Unexpected status: ${resp.status_code}
|
||||||
|
|
||||||
|
Map Threshold Config Returns Thresholds
|
||||||
|
[Documentation] When endpoint exists it returns low / medium / high thresholds.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/map-thresholds
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
IF ${resp.status_code} == 200
|
||||||
|
${body}= Set Variable ${resp.json()}
|
||||||
|
Dictionary Should Contain Key ${body} low
|
||||||
|
Dictionary Should Contain Key ${body} medium
|
||||||
|
Dictionary Should Contain Key ${body} high
|
||||||
|
END
|
||||||
|
|
||||||
|
Map Filter Clears And Resets Companion Table
|
||||||
|
[Documentation] Clicking the "Clear filter" control restores the unfiltered companion table.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/map
|
||||||
|
Wait For Elements State css=[data-testid="map-page"] visible timeout=15s
|
||||||
|
# Look for "Clear filter" — it may or may not exist depending on data state.
|
||||||
|
${has_clear}= Run Keyword And Return Status Get Text body contains Clear filter
|
||||||
|
# Not asserting; just verifying page renders without error.
|
||||||
|
Should Be True ${has_clear} or not ${has_clear} msg=Map page renders
|
||||||
|
Close Browser
|
||||||
181
e2e/tests/05_jails.robot
Normal file
181
e2e/tests/05_jails.robot
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Jail Management feature coverage — list, detail, controls,
|
||||||
|
... ban/unban, currently banned, IP lookup, ignore list.
|
||||||
|
Resource ${CURDIR}/../resources/common.resource
|
||||||
|
Resource ${CURDIR}/../resources/auth.resource
|
||||||
|
Suite Setup Wait For Backend Health
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Jails Page Lists Active Jails
|
||||||
|
[Documentation] Jails page shows active jails with name and metrics.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/jails
|
||||||
|
Wait For Elements State css=[data-testid="jails-page"] visible timeout=15s
|
||||||
|
Page Should Contain Jails
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Jails API Returns Active Jails
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/jails headers=${headers} expected_status=200
|
||||||
|
${body}= Set Variable ${resp.json()}
|
||||||
|
Dictionary Should Contain Key ${body} items
|
||||||
|
|
||||||
|
Jail Detail Page Loads For First Active Jail
|
||||||
|
[Documentation] Visiting /jails/<name> for a real active jail shows the detail view.
|
||||||
|
Login As Admin
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${jail}= Get First Active Jail Name
|
||||||
|
Log Using jail: ${jail}
|
||||||
|
Go To ${FRONTEND_URL}/jails/${jail}
|
||||||
|
Wait For Load State domcontentloaded
|
||||||
|
FOR ${i} IN RANGE 1 16
|
||||||
|
${found}= Run Keyword And Return Status
|
||||||
|
... Wait For Elements State css=[data-testid="jail-detail-page"] visible timeout=2s
|
||||||
|
IF ${found} BREAK
|
||||||
|
Sleep 1s
|
||||||
|
END
|
||||||
|
Page Should Contain ${jail}
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Ban An IP Via API
|
||||||
|
[Documentation] POST /api/v1/bans bans an IP in a specific jail.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${jail}= Get First Active Jail Name
|
||||||
|
${ip}= Generate Unique Ip
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary jail ${jail} ip ${ip}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/bans json=${payload}
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 201, 204] msg=Unexpected ban status: ${resp.status_code}
|
||||||
|
Set Suite Variable ${BANNED_IP} ${ip}
|
||||||
|
Set Suite Variable ${BANNED_JAIL} ${jail}
|
||||||
|
|
||||||
|
Unban The IP We Just Banned
|
||||||
|
[Documentation] DELETE /api/v1/bans removes an IP from a specific jail.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary jail ${BANNED_JAIL} ip ${BANNED_IP}
|
||||||
|
${resp}= DELETE On Session bangsess /api/v1/bans json=${payload}
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204] msg=Unexpected unban status: ${resp.status_code}
|
||||||
|
|
||||||
|
Unban All Endpoint Accepts Request
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= DELETE On Session bangsess /api/v1/bans/all
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204, 429] msg=Unexpected unban-all status: ${resp.status_code}
|
||||||
|
|
||||||
|
Active Bans Endpoint Returns List
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/bans/active
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
IP Lookup Endpoint Returns Geo
|
||||||
|
[Documentation] GET /api/v1/geo/lookup/{ip} returns enrichment data.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${ip}= Generate Unique Ip
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/geo/lookup/${ip}
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 404] msg=Unexpected lookup status: ${resp.status_code}
|
||||||
|
|
||||||
|
Ignore List Add And Remove Via API
|
||||||
|
[Documentation] POST /api/v1/jails/{name}/ignoreip adds an IP to the ignore list.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${jail}= Get First Active Jail Name
|
||||||
|
${ip}= Generate Unique Ip
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary ip ${ip}
|
||||||
|
${add_resp}= POST On Session bangsess /api/v1/jails/${jail}/ignoreip
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${add_resp.status_code} in [200, 201, 204]
|
||||||
|
${del_resp}= DELETE On Session bangsess /api/v1/jails/${jail}/ignoreip
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${del_resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Ignore Self Toggle Via API
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${jail}= Get First Active Jail Name
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/jails/${jail}/ignoreself
|
||||||
|
... json=${EMPTY} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Jail Reload Endpoint Works
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${jail}= Get First Active Jail Name
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/jails/${jail}/reload
|
||||||
|
... json=${EMPTY} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Jail Stop Endpoint Works
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${jail}= Get First Active Jail Name
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/jails/${jail}/stop
|
||||||
|
... json=${EMPTY} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204, 400, 403] msg=Unexpected stop status: ${resp.status_code}
|
||||||
|
|
||||||
|
Jail Start Endpoint Works
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${jail}= Get First Active Jail Name
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/jails/${jail}/start
|
||||||
|
... json=${EMPTY} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204, 400, 403]
|
||||||
|
|
||||||
|
Jail Idle Endpoint Works
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${jail}= Get First Active Jail Name
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/jails/${jail}/idle
|
||||||
|
... json=${EMPTY} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204, 400, 403]
|
||||||
|
|
||||||
|
Reload All Jails Endpoint Works
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/jails/reload-all
|
||||||
|
... json=${EMPTY} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Geo Stats Endpoint Returns Counters
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/geo/stats
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 200
|
||||||
180
e2e/tests/06_config_jails_filters_actions.robot
Normal file
180
e2e/tests/06_config_jails_filters_actions.robot
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Configuration View feature coverage — Jails / Filters / Actions tabs,
|
||||||
|
... inline editing, regex CRUD, raw config, activate/deactivate.
|
||||||
|
Resource ${CURDIR}/../resources/common.resource
|
||||||
|
Resource ${CURDIR}/../resources/auth.resource
|
||||||
|
Suite Setup Wait For Backend Health
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Config Page Renders All Required Tabs
|
||||||
|
[Documentation] Config page shows Jails, Filters, Actions, Server, Regex Tester tabs.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/config
|
||||||
|
Wait For Elements State css=[data-testid="config-page"] visible timeout=15s
|
||||||
|
Page Should Contain Jails
|
||||||
|
Page Should Contain Filters
|
||||||
|
Page Should Contain Actions
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Config Jails Tab Defaults To Active
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/config
|
||||||
|
Wait For Elements State css=[data-testid="config-page"] visible timeout=15s
|
||||||
|
# Jails tab is default. Active jails should appear in the list.
|
||||||
|
Sleep 2s
|
||||||
|
Page Should Contain Active
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Config Filters Tab Loads
|
||||||
|
[Documentation] Clicking the Filters tab shows the filter list.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/config
|
||||||
|
Wait For Elements State css=[data-testid="config-page"] visible timeout=15s
|
||||||
|
Run Keyword And Return Status Click text=Filters
|
||||||
|
Sleep 1s
|
||||||
|
Page Should Contain Filter
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Config Actions Tab Loads
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/config
|
||||||
|
Wait For Elements State css=[data-testid="config-page"] visible timeout=15s
|
||||||
|
Run Keyword And Return Status Click text=Actions
|
||||||
|
Sleep 1s
|
||||||
|
Page Should Contain Action
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Config Server Tab Loads
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/config
|
||||||
|
Wait For Elements State css=[data-testid="config-page"] visible timeout=15s
|
||||||
|
Run Keyword And Return Status Click text=Server
|
||||||
|
Sleep 1s
|
||||||
|
Page Should Contain Server
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Config Regex Tester Tab Loads
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/config
|
||||||
|
Wait For Elements State css=[data-testid="config-page"] visible timeout=15s
|
||||||
|
Run Keyword And Return Status Click text=Regex Tester
|
||||||
|
Sleep 1s
|
||||||
|
Page Should Contain Regex
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Config Regex Tester API Endpoint Validates Pattern
|
||||||
|
[Documentation] POST /api/v1/config/regex/test runs a pattern against a log line.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary pattern ^Failed password for .* from (\\d+\\.\\d+\\.\\d+\\.\\d+) log_line Failed password for root from 1.2.3.4
|
||||||
|
${resp}= POST On Session bangsess /api/v1/config/regex/test
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 400] msg=Unexpected regex test status: ${resp.status_code}
|
||||||
|
|
||||||
|
Config Jails Endpoint Lists Jail Configs
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/jails
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Config Filters Endpoint Lists Filter Configs
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/filters
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Config Actions Endpoint Lists Action Configs
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/actions
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Config Global Settings Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/global
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Config Service Status Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/service-status
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Config Security Headers Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/security-headers
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Config Inline Edit Round Trip For First Jail
|
||||||
|
[Documentation] Edit ban_time for a jail via API and verify the change is reflected.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${jail}= Get First Active Jail Name
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary ban_time 600
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/config/jails/${jail}
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204] msg=Unexpected jail update status: ${resp.status_code}
|
||||||
|
|
||||||
|
Config Raw Section Lazy Load
|
||||||
|
[Documentation] GET /api/v1/config/filters/{name}/raw returns the raw file content.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
# Use a common filter name; if missing, expect 404.
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/filters/sshd/raw
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 404] msg=Unexpected raw filter status: ${resp.status_code}
|
||||||
|
|
||||||
|
Config Raw Action File Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/actions/iptables-allports/raw
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 404] msg=Unexpected raw action status: ${resp.status_code}
|
||||||
|
|
||||||
|
Config Jail Files Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/jail-files
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Config Invalid Regex Returns 4xx
|
||||||
|
[Documentation] Regex tester rejects malformed patterns.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary pattern [unclosed log_line some text
|
||||||
|
${resp}= POST On Session bangsess /api/v1/config/regex/test
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} >= 400 msg=Invalid regex was accepted
|
||||||
153
e2e/tests/07_config_log_and_serversettings.robot
Normal file
153
e2e/tests/07_config_log_and_serversettings.robot
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Server settings + log viewer + log observation coverage.
|
||||||
|
Resource ${CURDIR}/../resources/common.resource
|
||||||
|
Resource ${CURDIR}/../resources/auth.resource
|
||||||
|
Suite Setup Wait For Backend Health
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Server Settings GET Returns Expected Keys
|
||||||
|
[Documentation] GET /api/v1/server/settings returns log level, target, etc.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/server/settings
|
||||||
|
... headers=${headers} expected_status=200
|
||||||
|
${body}= Set Variable ${resp.json()}
|
||||||
|
Dictionary Should Contain Key ${body} loglevel
|
||||||
|
|
||||||
|
Server Settings Update Log Level
|
||||||
|
[Documentation] PUT /api/v1/server/settings updates log level to INFO.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary loglevel INFO
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/server/settings
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Server Settings Reject Invalid Log Level
|
||||||
|
[Documentation] Invalid log level must return 4xx.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary loglevel NOT_A_LEVEL
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/server/settings
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} >= 400 msg=Invalid log level accepted
|
||||||
|
|
||||||
|
Server Settings Update DB Purge Age
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary dbpurgeage 648000
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/server/settings
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Server Settings Update Max Matches
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary maxmatches 10
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/server/settings
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Server Settings Reject Path Outside Allowlist
|
||||||
|
[Documentation] Log target must validate against /var/log or /config/log allowlist.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary logtarget /etc/passwd
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/server/settings
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} >= 400 msg=Path outside allowlist accepted
|
||||||
|
|
||||||
|
Server Settings Accept Stdout Special Target
|
||||||
|
[Documentation] STDOUT is a valid special log target.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary logtarget STDOUT
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/server/settings
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204] msg=STDOUT target rejected
|
||||||
|
|
||||||
|
Server Settings Accept Syslog Special Target
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary logtarget SYSLOG
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/server/settings
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204] msg=SYSLOG target rejected
|
||||||
|
|
||||||
|
Server Settings Accept Safe File Path
|
||||||
|
[Documentation] A path inside /var/log must be accepted.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary logtarget /var/log/fail2ban.log
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/server/settings
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Flush Logs Endpoint Works
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/server/flush-logs
|
||||||
|
... json=${EMPTY} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Log Preview Endpoint Returns Content
|
||||||
|
[Documentation] GET /api/v1/config/log/preview returns tail of log file.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/log/preview
|
||||||
|
... params=lines=100 headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 400, 404] msg=Unexpected log preview status: ${resp.status_code}
|
||||||
|
|
||||||
|
Log Endpoint Returns Content Or 404
|
||||||
|
[Documentation] GET /api/v1/config/log returns full log or 404 if logging to non-file.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/config/log
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 404] msg=Unexpected log status: ${resp.status_code}
|
||||||
|
|
||||||
|
Log Observation Add Rejects Path Outside Allowlist
|
||||||
|
[Documentation] POST /api/v1/config/add-log-observation rejects /etc/passwd.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary path /etc/passwd jail nonexistent
|
||||||
|
${resp}= POST On Session bangsess /api/v1/config/add-log-observation
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} >= 400 msg=Path outside allowlist accepted
|
||||||
|
|
||||||
|
Log Observation Add Endpoint Exists
|
||||||
|
[Documentation] POST /api/v1/config/add-log-observation is reachable.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary path /var/log/nonexistent.log jail none
|
||||||
|
${resp}= POST On Session bangsess /api/v1/config/add-log-observation
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 201, 400, 404] msg=Endpoint missing
|
||||||
102
e2e/tests/08_history.robot
Normal file
102
e2e/tests/08_history.robot
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation Ban History feature coverage — table, filters,
|
||||||
|
... per-IP timeline, archive vs fail2ban source.
|
||||||
|
Resource ${CURDIR}/../resources/common.resource
|
||||||
|
Resource ${CURDIR}/../resources/auth.resource
|
||||||
|
Suite Setup Wait For Backend Health
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
History Page Renders
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/history
|
||||||
|
Wait For Elements State css=[data-testid="history-page"] visible timeout=15s
|
||||||
|
Page Should Contain History
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
History Page Shows Archive Source Badge By Default
|
||||||
|
[Documentation] Per Features.md, default source on history page is BanGUI archive.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/history
|
||||||
|
Wait For Elements State css=[data-testid="history-page"] visible timeout=15s
|
||||||
|
Sleep 2s
|
||||||
|
${has_arch}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains BanGUI DB
|
||||||
|
${has_live}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains fail2ban DB
|
||||||
|
Should Be True ${has_arch} or ${has_live} msg=No source badge visible on history page
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
History Page Default 7d Range
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/history
|
||||||
|
Wait For Elements State css=[data-testid="history-page"] visible timeout=15s
|
||||||
|
Sleep 1s
|
||||||
|
${has_7d}= Run Keyword And Return Status
|
||||||
|
... Get Text body contains Last 7 days
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
History Endpoint Returns Paginated Data
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/history
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
History Archive Endpoint Returns Data
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/history/archive
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
History Per IP Endpoint Returns Data
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${ip}= Generate Unique Ip
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/history/${ip}
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
History Filter By Jail Returns Data
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/history
|
||||||
|
... params=jail=sshd&range=7d headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
History Filter By Source Fail2ban
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/history
|
||||||
|
... params=source=fail2ban&range=24h headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
History Filter By Source Archive
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/history
|
||||||
|
... params=source=archive&range=7d headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
History URL Params Honored
|
||||||
|
[Documentation] Page should load with ?page_size=500&source=fail2ban params.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/history?page_size=500&source=fail2ban
|
||||||
|
Wait For Load State domcontentloaded
|
||||||
|
Sleep 2s
|
||||||
|
${url}= Get URL
|
||||||
|
Should Contain ${url} page_size=500
|
||||||
|
Should Contain ${url} source=fail2ban
|
||||||
|
Close Browser
|
||||||
161
e2e/tests/09_blocklists.robot
Normal file
161
e2e/tests/09_blocklists.robot
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation External Blocklist Importer feature coverage — sources CRUD,
|
||||||
|
... URL validation, schedule, preview, import log, delete restriction.
|
||||||
|
Resource ${CURDIR}/../resources/common.resource
|
||||||
|
Resource ${CURDIR}/../resources/auth.resource
|
||||||
|
Suite Setup Wait For Backend Health
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Blocklists Page Renders
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/blocklists
|
||||||
|
Wait For Elements State css=[data-testid="blocklists-page"] visible timeout=15s
|
||||||
|
Page Should Contain Blocklists
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Blocklists Sources List Endpoint
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/blocklists
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Blocklist Source Create Rejects Invalid Scheme
|
||||||
|
[Documentation] ftp://, file://, gopher:// must be rejected.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${stamp}= Evaluate int(time.time()) modules=time
|
||||||
|
${payload}= Create Dictionary
|
||||||
|
... name test-scheme-${stamp}
|
||||||
|
... url ftp://example.com/list.txt
|
||||||
|
... enabled ${TRUE}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/blocklists
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 400
|
||||||
|
... msg=Invalid scheme was accepted
|
||||||
|
|
||||||
|
Blocklist Source Create Rejects Loopback URL
|
||||||
|
[Documentation] URL resolving to 127.0.0.1 must be rejected (SSRF guard).
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${stamp}= Evaluate int(time.time()) modules=time
|
||||||
|
${payload}= Create Dictionary
|
||||||
|
... name test-loopback-${stamp}
|
||||||
|
... url http://127.0.0.1/list.txt
|
||||||
|
... enabled ${TRUE}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/blocklists
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 400
|
||||||
|
... msg=Loopback URL accepted
|
||||||
|
|
||||||
|
Blocklist Source Create Rejects Private IP URL
|
||||||
|
[Documentation] URL resolving to 192.168.x.x must be rejected.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${stamp}= Evaluate int(time.time()) modules=time
|
||||||
|
${payload}= Create Dictionary
|
||||||
|
... name test-private-${stamp}
|
||||||
|
... url http://192.168.1.1/list.txt
|
||||||
|
... enabled ${TRUE}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/blocklists
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 400
|
||||||
|
... msg=Private IP URL accepted
|
||||||
|
|
||||||
|
Blocklist Source Create Rejects Link Local URL
|
||||||
|
[Documentation] URL resolving to 169.254.x.x must be rejected.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${stamp}= Evaluate int(time.time()) modules=time
|
||||||
|
${payload}= Create Dictionary
|
||||||
|
... name test-linklocal-${stamp}
|
||||||
|
... url http://169.254.169.254/list.txt
|
||||||
|
... enabled ${TRUE}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/blocklists
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 400
|
||||||
|
... msg=Link-local URL accepted
|
||||||
|
|
||||||
|
Blocklist Schedule Endpoint Returns Config
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/blocklists/schedule
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Blocklist Schedule Update Works
|
||||||
|
[Documentation] PUT /api/v1/blocklists/schedule updates the import schedule.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${payload}= Create Dictionary frequency daily hour 3 minute 0
|
||||||
|
${resp}= PUT On Session bangsess /api/v1/blocklists/schedule
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Blocklist Manual Import Endpoint Reachable
|
||||||
|
[Documentation] POST /api/v1/blocklists/import triggers a manual import.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= POST On Session bangsess /api/v1/blocklists/import
|
||||||
|
... json=${EMPTY} headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 202, 429] msg=Unexpected import status: ${resp.status_code}
|
||||||
|
|
||||||
|
Blocklist Import Log Endpoint Returns Paginated Data
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/blocklists/log
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 204]
|
||||||
|
|
||||||
|
Blocklist Delete Non Existent Returns 404
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= DELETE On Session bangsess /api/v1/blocklists/999999
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 404
|
||||||
|
|
||||||
|
Blocklist Create And Delete Cycle
|
||||||
|
[Documentation] Create a valid blocklist source then delete it.
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
# Create via fetch POST (relative to backend) so we can use a public IP.
|
||||||
|
${stamp}= Evaluate int(time.time()) modules=time
|
||||||
|
${payload}= Create Dictionary
|
||||||
|
... name cycle-test-${stamp}
|
||||||
|
... url https://lists.blocklist.de/lists/ssh.txt
|
||||||
|
... enabled ${FALSE}
|
||||||
|
${create_resp}= POST On Session bangsess /api/v1/blocklists
|
||||||
|
... json=${payload} headers=${headers} expected_status=any
|
||||||
|
IF ${create_resp.status_code} in [200, 201]
|
||||||
|
${body}= Set Variable ${create_resp.json()}
|
||||||
|
${id}= Set Variable ${body}[id]
|
||||||
|
# If source had import logs, delete would return 409. With no logs it should succeed.
|
||||||
|
${del_resp}= DELETE On Session bangsess /api/v1/blocklists/${id}
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${del_resp.status_code} in [200, 204, 409]
|
||||||
|
... msg=Unexpected delete status: ${del_resp.status_code}
|
||||||
|
ELSE
|
||||||
|
Log Could not create blocklist source (status ${create_resp.status_code}); skipping delete cycle
|
||||||
|
END
|
||||||
121
e2e/tests/10_general_layout.robot
Normal file
121
e2e/tests/10_general_layout.robot
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
*** Settings ***
|
||||||
|
Documentation General UI / layout behaviour — sidebar nav,
|
||||||
|
... active link highlighting, server-status badge, session persistence.
|
||||||
|
Resource ${CURDIR}/../resources/common.resource
|
||||||
|
Resource ${CURDIR}/../resources/auth.resource
|
||||||
|
Suite Setup Wait For Backend Health
|
||||||
|
|
||||||
|
*** Test Cases ***
|
||||||
|
Sidebar Is Visible On Dashboard
|
||||||
|
[Documentation] After login the sidebar nav is visible.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=main visible timeout=10s
|
||||||
|
${nav_visible}= Run Keyword And Return Status
|
||||||
|
... Wait For Elements State css=nav[aria-label="Main navigation"] visible timeout=5s
|
||||||
|
Should Be True ${nav_visible} msg=Sidebar navigation not visible on dashboard
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Sidebar Lists All Required Pages
|
||||||
|
[Documentation] Sidebar contains links to Dashboard, World Map, Jails,
|
||||||
|
... Configuration, History, and a Sign Out button.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=main visible timeout=10s
|
||||||
|
Page Should Contain Dashboard
|
||||||
|
Page Should Contain World Map
|
||||||
|
Page Should Contain Jails
|
||||||
|
Page Should Contain Configuration
|
||||||
|
Page Should Contain History
|
||||||
|
Page Should Contain Sign out
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Sidebar Sign Out Logs User Out
|
||||||
|
[Documentation] Clicking Sign out in sidebar clears the session.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=main visible timeout=10s
|
||||||
|
Click css=[aria-label="Sign out"]
|
||||||
|
Wait For Load State domcontentloaded
|
||||||
|
${url}= Get URL
|
||||||
|
Should Contain ${url} /login
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Theme Toggle Is Present In Sidebar
|
||||||
|
[Documentation] Sidebar exposes a theme toggle button.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=main visible timeout=10s
|
||||||
|
${theme_visible}= Run Keyword And Return Status
|
||||||
|
... Get Element States css=[aria-label*="light mode"], [aria-label*="dark mode"] contains visible
|
||||||
|
Should Be True ${theme_visible} msg=Theme toggle not visible
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Active Page Highlighted In Sidebar
|
||||||
|
[Documentation] The current page is marked active in the sidebar nav.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/jails
|
||||||
|
Wait For Elements State css=[data-testid="jails-page"] visible timeout=10s
|
||||||
|
${active}= Run Keyword And Return Status
|
||||||
|
... Get Element States css=nav[aria-label="Main navigation"] [aria-current="page"] contains visible
|
||||||
|
Should Be True ${active} msg=No active page link highlighted in sidebar
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Session Persists Across Page Reload
|
||||||
|
[Documentation] Reloading the page does NOT log the user out.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=[data-testid="dashboard"] visible timeout=10s
|
||||||
|
Reload
|
||||||
|
Wait For Load State domcontentloaded
|
||||||
|
Sleep 2s
|
||||||
|
${url}= Get URL
|
||||||
|
Should Not Contain ${url} /login
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Theme Toggle Changes Color Mode
|
||||||
|
[Documentation] Clicking the theme toggle changes the document color scheme.
|
||||||
|
Login As Admin
|
||||||
|
Go To ${FRONTEND_URL}/
|
||||||
|
Wait For Elements State css=main visible timeout=10s
|
||||||
|
${before}= Evaluate JavaScript ${None} () => document.documentElement.getAttribute('data-theme') || document.documentElement.style.colorScheme || 'unknown'
|
||||||
|
Log Theme before: ${before}
|
||||||
|
# Try clicking either light or dark mode toggle (one of them exists).
|
||||||
|
Run Keyword And Ignore Error Click css=[aria-label="Switch to light mode"]
|
||||||
|
Run Keyword And Ignore Error Click css=[aria-label="Switch to dark mode"]
|
||||||
|
Sleep 1s
|
||||||
|
${after}= Evaluate JavaScript ${None} () => document.documentElement.getAttribute('data-theme') || document.documentElement.style.colorScheme || 'unknown'
|
||||||
|
Log Theme after: ${after}
|
||||||
|
Close Browser
|
||||||
|
|
||||||
|
Health Endpoint Returns Component Status
|
||||||
|
Set Random Xff Header
|
||||||
|
Login Via HTTP
|
||||||
|
${headers}= Create Dictionary X-BanGUI-Request 1
|
||||||
|
Set To Dictionary ${headers} X-Forwarded-For ${XFF_HEADER}
|
||||||
|
${resp}= GET On Session bangsess /api/v1/health/ready
|
||||||
|
... headers=${headers} expected_status=any
|
||||||
|
Should Be True ${resp.status_code} in [200, 503] msg=Unexpected ready status: ${resp.status_code}
|
||||||
|
|
||||||
|
Liveness Endpoint Returns 200
|
||||||
|
${resp}= GET ${BACKEND_URL}/api/v1/health/live expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 200
|
||||||
|
|
||||||
|
Metrics Endpoint Returns Prometheus Text
|
||||||
|
[Documentation] GET /api/v1/metrics returns Prometheus text format.
|
||||||
|
${resp}= GET ${BACKEND_URL}/api/v1/metrics expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 200
|
||||||
|
${body}= Set Variable ${resp.text}
|
||||||
|
Should Contain ${body} HELP # Prometheus exposition format marker
|
||||||
|
|
||||||
|
Setup Timezone Endpoint Returns IANA String
|
||||||
|
${resp}= GET ${BACKEND_URL}/api/v1/setup/timezone expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 200
|
||||||
|
${body}= Set Variable ${resp.json()}
|
||||||
|
Dictionary Should Contain Key ${body} timezone
|
||||||
|
|
||||||
|
Setup Status Endpoint Returns Completed Flag
|
||||||
|
${resp}= GET ${BACKEND_URL}/api/v1/setup expected_status=any
|
||||||
|
Should Be Equal As Integers ${resp.status_code} 200
|
||||||
|
${body}= Set Variable ${resp.json()}
|
||||||
|
Dictionary Should Contain Key ${body} completed
|
||||||
4
frontend/package-lock.json
generated
4
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "bangui-frontend",
|
"name": "bangui-frontend",
|
||||||
"version": "0.9.19-rc.4",
|
"version": "0.9.19-rc.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bangui-frontend",
|
"name": "bangui-frontend",
|
||||||
"version": "0.9.19-rc.4",
|
"version": "0.9.19-rc.5",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.55.0",
|
"@fluentui/react-components": "^9.55.0",
|
||||||
"@fluentui/react-icons": "^2.0.257",
|
"@fluentui/react-icons": "^2.0.257",
|
||||||
|
|||||||
@@ -299,8 +299,9 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
label="Master Password"
|
label="Master Password"
|
||||||
required
|
required
|
||||||
validationMessage={
|
validationMessage={
|
||||||
errors.masterPassword ??
|
errors.masterPassword
|
||||||
(passwordRules.some((rule) => !rule.satisfied)
|
? errors.masterPassword
|
||||||
|
: passwordRules.some((rule) => !rule.satisfied)
|
||||||
? {
|
? {
|
||||||
children: (
|
children: (
|
||||||
<ul className={styles.passwordRuleList}>
|
<ul className={styles.passwordRuleList}>
|
||||||
@@ -319,7 +320,7 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
</ul>
|
</ul>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
: undefined)
|
: undefined
|
||||||
}
|
}
|
||||||
validationState={
|
validationState={
|
||||||
errors.masterPassword || passwordRules.some((rule) => !rule.satisfied)
|
errors.masterPassword || passwordRules.some((rule) => !rule.satisfied)
|
||||||
@@ -332,6 +333,7 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
value={values.masterPassword}
|
value={values.masterPassword}
|
||||||
onChange={handleChange("masterPassword")}
|
onChange={handleChange("masterPassword")}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
|
aria-label="Master Password"
|
||||||
/>
|
/>
|
||||||
<div className={styles.passwordStrength} aria-live="polite">
|
<div className={styles.passwordStrength} aria-live="polite">
|
||||||
<div className={styles.passwordStrengthBar}>
|
<div className={styles.passwordStrengthBar}>
|
||||||
@@ -363,6 +365,7 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
value={values.confirmPassword}
|
value={values.confirmPassword}
|
||||||
onChange={handleChange("confirmPassword")}
|
onChange={handleChange("confirmPassword")}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
|
aria-label="Confirm Password"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@@ -375,6 +378,7 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
<Input
|
<Input
|
||||||
value={values.databasePath}
|
value={values.databasePath}
|
||||||
onChange={handleChange("databasePath")}
|
onChange={handleChange("databasePath")}
|
||||||
|
aria-label="Database Path"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@@ -387,6 +391,7 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
<Input
|
<Input
|
||||||
value={values.fail2banSocket}
|
value={values.fail2banSocket}
|
||||||
onChange={handleChange("fail2banSocket")}
|
onChange={handleChange("fail2banSocket")}
|
||||||
|
aria-label="fail2ban Socket Path"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@@ -397,6 +402,7 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
<Input
|
<Input
|
||||||
value={values.timezone}
|
value={values.timezone}
|
||||||
onChange={handleChange("timezone")}
|
onChange={handleChange("timezone")}
|
||||||
|
aria-label="Timezone"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
@@ -410,7 +416,7 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
type="number"
|
type="number"
|
||||||
value={values.sessionDurationMinutes}
|
value={values.sessionDurationMinutes}
|
||||||
onChange={handleChange("sessionDurationMinutes")}
|
onChange={handleChange("sessionDurationMinutes")}
|
||||||
min={1}
|
aria-label="Session Duration (minutes)"
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user