Compare commits
13 Commits
main
...
408eb900eb
| Author | SHA1 | Date | |
|---|---|---|---|
| 408eb900eb | |||
| 407ca83850 | |||
| 72273ca945 | |||
| 9e59fc8bae | |||
| ef8feba4b2 | |||
| 5a12d1c22f | |||
| aebe0d0236 | |||
| 99e1b74405 | |||
| 9fe52755a5 | |||
| 9d2d6fadf3 | |||
| 2e5ac092bf | |||
| dcee222a41 | |||
| 12fe70d768 |
@@ -48,7 +48,6 @@ 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. {item}");
|
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}");
|
||||||
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,18 +274,7 @@ 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,12 +318,7 @@ 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,9 +26,10 @@ 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 _cleanup_wal_files, init_db, open_db
|
from app.db import 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
|
||||||
@@ -47,7 +48,6 @@ 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,7 +98,9 @@ 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(f"BANGUI_WORKERS environment variable must be an integer, got: {workers_env}") from e
|
raise RuntimeError(
|
||||||
|
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:
|
||||||
@@ -331,11 +333,6 @@ 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)
|
||||||
|
|
||||||
@@ -360,7 +357,9 @@ 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 = await setup_service.get_persisted_runtime_settings(runtime_db)
|
persisted_runtime_settings = (
|
||||||
|
await setup_service.get_persisted_runtime_settings(runtime_db)
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
await runtime_db.close()
|
await runtime_db.close()
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,8 @@
|
|||||||
# 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
|
||||||
@@ -44,17 +14,10 @@ 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/02_login.robot
|
robot --outputdir results tests/01_page_loading.robot
|
||||||
robot --outputdir results tests/08_history.robot
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run with Browser Visible
|
## Run with Browser Visible
|
||||||
@@ -63,42 +26,10 @@ robot --outputdir results tests/08_history.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
|
||||||
|
|||||||
93
e2e/playwright-log.txt
Normal file
93
e2e/playwright-log.txt
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
{"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"}
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
#!/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()
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
*** 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,22 +1,10 @@
|
|||||||
*** 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 ${TEST_PASSWORD}
|
${login_payload}= Create Dictionary password Hallo123!
|
||||||
${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
|
||||||
@@ -34,7 +22,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=${TEST_PASSWORD}
|
... master_password=Hallo123!
|
||||||
... 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
|
||||||
@@ -62,7 +50,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: '${TEST_PASSWORD}' }),
|
... body: JSON.stringify({ password: 'Hallo123!' }),
|
||||||
... credentials: 'include'
|
... credentials: 'include'
|
||||||
... });
|
... });
|
||||||
... const data = await res.json().catch(() => ({}));
|
... const data = await res.json().catch(() => ({}));
|
||||||
@@ -111,61 +99,4 @@ Login As Admin
|
|||||||
END
|
END
|
||||||
|
|
||||||
${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,97 +2,20 @@
|
|||||||
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/live expected_status=any
|
${response}= GET ${BACKEND_URL}/api/v1/health expected_status=200
|
||||||
IF ${response.status_code} == 200 BREAK
|
IF ${response.status} == 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)
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
*** 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}
|
|
||||||
127
e2e/tests/01_page_loading.robot
Normal file
127
e2e/tests/01_page_loading.robot
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
*** 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
|
||||||
@@ -35,14 +35,13 @@ 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 — fail2ban: confirm IP is banned in manual-Jail
|
# Step 3 — backend API: confirm ban via Python in fail2ban container.
|
||||||
${resp}= Run Process
|
# Browser (Playwright) and host shell have same IP, hitting GlobalRateLimiter.
|
||||||
... bash
|
# fail2ban container has a different source IP, so its requests bypass the limit.
|
||||||
... -c
|
# Container reaches backend via host network (localhost:8000).
|
||||||
... 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"
|
${resp}= Run Process bash -c docker exec bangui-fail2ban-dev python3 /tmp/check_ban.py timeout=15s
|
||||||
... timeout=15s
|
|
||||||
${resp_text}= Set Variable ${resp.stdout}
|
${resp_text}= Set Variable ${resp.stdout}
|
||||||
Log fail2ban status: ${resp_text}
|
Log API response: ${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
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
*** 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
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
*** 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]
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
*** 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
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
*** 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
|
|
||||||
@@ -8,8 +8,6 @@ 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
|
||||||
|
|
||||||
@@ -33,31 +31,37 @@ 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
|
||||||
|
|
||||||
# Verify initial strength text shows "0 of 4 rules satisfied".
|
# Initially no segments are active — no rules satisfied.
|
||||||
${text_0}= Get Text xpath=//div[@aria-live="polite"]
|
${segments}= Get Elements css=.passwordStrengthSegment
|
||||||
Should Contain ${text_0} 0 of 4 rules satisfied
|
${active_count}= Set Variable 0
|
||||||
Log Initial strength: ${text_0}
|
FOR ${seg} IN @{segments}
|
||||||
|
${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"] longpassword
|
Fill Text css=input[aria-label="Master Password"] WeakPass
|
||||||
|
${active_count}= Set Variable 0
|
||||||
# Verify strength text updates to "1 of 4 rules satisfied" (only length rule, no uppercase/number/special).
|
${segments}= Get Elements css=.passwordStrengthSegment
|
||||||
${text_1}= Get Text xpath=//div[@aria-live="polite"]
|
FOR ${seg} IN @{segments}
|
||||||
Should Contain ${text_1} 1 of 4 rules satisfied
|
${classes}= Get Attribute ${seg} class
|
||||||
Log After longpassword: ${text_1}
|
IF "Active" in """${classes}"""
|
||||||
|
${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
|
||||||
|
|
||||||
@@ -65,8 +69,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 xpath=//*[@aria-label="Confirm Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"] visible timeout=10s
|
Wait For Elements State css=[aria-label="Confirm Password"] attached timeout=5s
|
||||||
${msg}= Get Text xpath=//*[@aria-label="Confirm Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"] timeout=10s
|
${msg}= Get Text css=[aria-label="Confirm Password"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
||||||
Should Be Equal As Strings ${msg} Passwords do not match.
|
Should Be Equal As Strings ${msg} Passwords do not match.
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
@@ -74,23 +78,21 @@ 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 xpath=//*[@aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
${msg}= Get Text css=[aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
||||||
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 xpath=//*[@aria-label="Database Path"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
${msg}= Get Text css=[aria-label="Database Path"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
||||||
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 xpath=//*[@aria-label="fail2ban Socket Path"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
${msg}= Get Text css=[aria-label="fail2ban Socket Path"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
||||||
Should Be Equal As Strings ${msg} Socket path is required.
|
Should Be Equal As Strings ${msg} Socket path is required.
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
@@ -98,8 +100,6 @@ 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 xpath=//*[@aria-label="Session Duration (minutes)"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
${msg}= Get Text css=[aria-label="Session Duration (minutes)"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
||||||
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,16 +120,14 @@ 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 xpath=//*[@aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"] visible timeout=10s
|
Wait For Elements State css=[aria-label="Master Password"] attached timeout=5s
|
||||||
${msg}= Get Text xpath=//*[@aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[@role="alert"]
|
${msg}= Get Text css=[aria-label="Master Password"]/ancestor::*[contains(@class,"field")]//*[contains(@class,"validationMessage")]
|
||||||
Should Contain ${msg} Password must meet all complexity requirements.
|
Should Contain ${msg} Password must meet all complexity requirements.
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
@@ -137,13 +135,11 @@ 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/v1/setup
|
${status_resp}= GET ${BACKEND_URL}/api/setup/status
|
||||||
${status_body}= Set Variable ${status_resp.json()}
|
${status_body}= Set Variable ${status_resp.json()}
|
||||||
Log Setup complete: ${status_body}[completed]
|
Log Setup complete: ${status_body}[setup_complete]
|
||||||
|
|
||||||
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
|
||||||
@@ -172,8 +168,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/v1/setup
|
${new_status_resp}= GET ${BACKEND_URL}/api/setup/status
|
||||||
${new_status_body}= Set Variable ${new_status_resp.json()}
|
${new_status_body}= Set Variable ${new_status_resp.json()}
|
||||||
Should Be True ${new_status_body}[completed]
|
Should Be True ${new_status_body}[setup_complete]
|
||||||
|
|
||||||
Close Browser
|
Close Browser
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
*** 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
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
*** 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
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
*** 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
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
*** 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
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
*** 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.5",
|
"version": "0.9.19-rc.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bangui-frontend",
|
"name": "bangui-frontend",
|
||||||
"version": "0.9.19-rc.5",
|
"version": "0.9.19-rc.4",
|
||||||
"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,28 +299,27 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
label="Master Password"
|
label="Master Password"
|
||||||
required
|
required
|
||||||
validationMessage={
|
validationMessage={
|
||||||
errors.masterPassword
|
errors.masterPassword ??
|
||||||
? errors.masterPassword
|
(passwordRules.some((rule) => !rule.satisfied)
|
||||||
: passwordRules.some((rule) => !rule.satisfied)
|
? {
|
||||||
? {
|
children: (
|
||||||
children: (
|
<ul className={styles.passwordRuleList}>
|
||||||
<ul className={styles.passwordRuleList}>
|
{passwordRules.map((rule) => (
|
||||||
{passwordRules.map((rule) => (
|
<li
|
||||||
<li
|
key={rule.id}
|
||||||
key={rule.id}
|
className={
|
||||||
className={
|
rule.satisfied
|
||||||
rule.satisfied
|
? styles.passwordRuleItemPassed
|
||||||
? styles.passwordRuleItemPassed
|
: styles.passwordRuleItemFailed
|
||||||
: styles.passwordRuleItemFailed
|
}
|
||||||
}
|
>
|
||||||
>
|
{rule.label}
|
||||||
{rule.label}
|
</li>
|
||||||
</li>
|
))}
|
||||||
))}
|
</ul>
|
||||||
</ul>
|
),
|
||||||
),
|
}
|
||||||
}
|
: undefined)
|
||||||
: undefined
|
|
||||||
}
|
}
|
||||||
validationState={
|
validationState={
|
||||||
errors.masterPassword || passwordRules.some((rule) => !rule.satisfied)
|
errors.masterPassword || passwordRules.some((rule) => !rule.satisfied)
|
||||||
@@ -333,7 +332,6 @@ 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}>
|
||||||
@@ -365,7 +363,6 @@ 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>
|
||||||
|
|
||||||
@@ -378,7 +375,6 @@ 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>
|
||||||
|
|
||||||
@@ -391,7 +387,6 @@ 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>
|
||||||
|
|
||||||
@@ -402,7 +397,6 @@ 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>
|
||||||
|
|
||||||
@@ -416,7 +410,7 @@ export function SetupPage(): React.JSX.Element {
|
|||||||
type="number"
|
type="number"
|
||||||
value={values.sessionDurationMinutes}
|
value={values.sessionDurationMinutes}
|
||||||
onChange={handleChange("sessionDurationMinutes")}
|
onChange={handleChange("sessionDurationMinutes")}
|
||||||
aria-label="Session Duration (minutes)"
|
min={1}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user