Skip to main content

Admin Management — Testing

Strategy

LayerToolingScope
UnitpytestJWT validation, ban-check logic, announcement expiry helper
Integrationpytest + HTTPX + SQLite in-memAll /admin/* endpoints, middleware logging, background loops
E2EPlaywright (admin-frontend)Admin login → user ban → verify banned user rejected on mobile login

Coverage Targets

AreaTarget
Admin auth / JWT validation100%
User CRUD endpoints100%
Ban creation and enforcement100%
Announcement CRUD + expiry90%+
Activity log middleware90%+
Router telemetry endpoint80%+
Overall admin feature coverage≥ 80%

Mocking Rules

  • Database — pytest fixtures with an in-memory SQLite database; swap MariaDB dialect.
  • MikroTik API — mock fetch_mikrotik_metrics; return a fixed payload; never hit real hardware.
  • Background loops — call expire_announcements_loop and collect_metrics_loop directly in tests rather than running the infinite loop.
  • Time — use freezegun to control datetime.utcnow() in ban and expiry assertions.
  • Admin JWT — generate test tokens with a known secret; use a fixture helper admin_token().

Test Cases

Authentication

ScenarioExpected result
POST /admin/login with valid credentialsReturns { access_token, token_type: "bearer", expires_in: 3600 }
POST /admin/login with wrong passwordReturns 401
POST /admin/login with unknown usernameReturns 401
Request to any /admin/* with no tokenReturns 401
Request to /admin/* with regular user JWTReturns 403
Request to /admin/* with expired admin JWTReturns 401

User CRUD

ScenarioExpected result
GET /admin/usersReturns paginated list with id, username, role, is_active per user
GET /admin/users?page=2&limit=5Returns correct page slice
POST /admin/users with valid payloadUser row created; response contains new user id
POST /admin/users with duplicate usernameReturns 409
PATCH /admin/users/{id} update display_nameRow updated; response reflects new value
DELETE /admin/users/{id}is_active set to false; row not removed from DB
DELETE /admin/users/{non_existent_id}Returns 404

Role Management

ScenarioExpected result
PATCH /admin/users/{id}/role with { role: "rescuer" }users.role updated to rescuer
PATCH /admin/users/{id}/role with { role: "user" }users.role updated to user
Admin attempts to change own roleReturns 403
PATCH /admin/users/{id}/role with invalid role valueReturns 422

Ban Management

ScenarioExpected result
POST /admin/users/{id}/ban with reason and future untilbanneduser row created; until and reason match payload
Banned user calls POST /auth/login before untilReturns 429 with { reason, until }
Banned user calls POST /auth/login after until (past expiry)Login succeeds normally
DELETE /admin/users/{id}/ban while ban activeBan until set to now(); subsequent login succeeds
GET /admin/users/{id}/bansReturns full ban history including past bans
POST /admin/users/{id}/ban when user already has active banOverwrites or appends depending on policy; at most one active ban enforced

Announcements

ScenarioExpected result
POST /admin/announcements with priority: "high", audience: "all", no expires_atRow created; is_expired: false
POST /admin/announcements with expires_at in the pastRow created; is_expired set to true by loop on next run
expire_announcements_loop runs with one expired rowThat row's is_expired set to true; non-expired rows unchanged
GET /announcements as rescuerReturns announcements with audience: all and audience: rescuers; excludes audience: users
GET /announcements as regular userReturns audience: all and audience: users; excludes audience: rescuers
GET /announcements with expired announcementExpired row excluded from response
PATCH /admin/announcements/{id} update bodyBody updated; updated_at refreshed
DELETE /admin/announcements/{id}Row removed; subsequent GET does not include it

Activity Logs

ScenarioExpected result
Any request to /admin/* completesOne row added to activity_logs with matching action, status_code, ip
Failed request (returns 4xx)Log row still created with the error status_code
GET /admin/logsReturns paginated activity log entries
GET /admin/logs?user_id=XReturns only entries where user_id = X
GET /admin/logs?action=user.banReturns only entries with action = user.ban
Log write failureRequest completes successfully; failure logged to stderr; no 500 returned to client

Router Telemetry

ScenarioExpected result
collect_metrics_loop runs oncerouterhealth and interfacetraffic rows inserted
GET /admin/router/stats with rows in DBReturns most recent cpu_percent, memory_*, uptime_seconds, interfaces array
GET /admin/router/stats with no rows in DBReturns 200 with empty/null values; no 500
MikroTik API unreachable during loop runLoop logs error; skips insert; retries on next interval

Test File Locations

server/
tests/
test_admin_auth.py
test_admin_users.py
test_admin_bans.py
test_admin_announcements.py
test_admin_logs.py
test_admin_router_stats.py

admin-frontend/
e2e/
admin-ban-flow.spec.ts