Development & Testing¶
This page covers how to set up and run the Gasket Gateway test suite.
Overview¶
Gasket has two test scripts. Both run inside Docker containers — no host-side Python, Chromium, or test dependencies required.
| Script | What it does | OIDC | Starts Gasket? | Requires |
|---|---|---|---|---|
test.sh |
Tests app logic with OIDC bypassed | ❌ Mocked (test mode) | ✅ Own instance (docker-compose-test.yaml) |
Docker only |
test-oidc.sh |
Tests real OIDC login flow via Authentik | ✅ Real OIDC | ✅ Own instance (docker-compose-dev.yaml) |
Full dev environment + provision.sh |
Both scripts are self-contained — they build, start, test, and tear down automatically. Previous test results and logs are cleared on each run.
Key Differences¶
test.sh layers docker-compose-test.yaml on top of docker-compose-dev.yaml, which sets GASKET_TEST_MODE=1. This bypasses OIDC and injects a mock admin session (user3 equivalent) for every request, so tests can exercise all portal and admin functionality without Authentik running.
test-oidc.sh layers docker-compose-test-runner.yaml on top of docker-compose-dev.yaml — it does not set GASKET_TEST_MODE, so the real OIDC flow through Authentik is used. This requires the full development environment (Traefik, Authentik, etc.) to be running first.
General Tests (test.sh)¶
Running¶
What's Tested¶
/healthendpoint returns 200- Portal page loads and displays test user
- Dedicated
/keyspage loads - Admin
/adminredirects to/admin/status - Admin sub-pages load:
/admin/status,/admin/backends,/admin/profiles,/admin/keys,/admin/policies - Admin status page contains connection status
- UI demo page loads
- Backend CRUD, admin page, and status API
- Profile CRUD, admin page, and config profile handling
- Policy CRUD, versioning, acceptance, reacceptance, and config policy handling
- API key create, read, reveal, edit, revoke, restore, lifecycle, admin management, and policy snapshots
How Test Mode Works¶
When GASKET_TEST_MODE is set, create_app() in __init__.py:
- Skips OIDC initialisation entirely
- Registers a
before_requesthandler that injects a session with:user_email:user3@localhostuser_name:user3user_groups:["gasket-users", "gasket-admins"]
OIDC Flow Tests (test-oidc.sh)¶
OIDC tests validate the full authentication flow against a live Authentik instance using:
- ROPC (Resource Owner Password Credentials) —
client_credentialsgrant with app password tokens - Selenium (Chromium headless) — browser-based login through the Authentik UI (with Shadow DOM piercing)
Prerequisites¶
-
The full development environment must be running:
-
Authentik must be provisioned with ROPC and implicit consent:
The provisioning script creates app password tokens for test users and configures implicit consent on the Gasket Gateway provider.
Running¶
What's Tested¶
Login Flow (test_oidc_login.py)¶
| Test | Description |
|---|---|
| ROPC token exchange | user2 and user3 receive valid tokens via app passwords |
| user1 denied | user1 (not in gasket-users) gets rejected |
| Invalid password | Wrong credentials are rejected |
| Unauthenticated redirect | Accessing / without login redirects to Authentik |
| Browser login (user2) | Selenium login through Authentik UI + portal access |
| Admin access (user3) | Admin panel accessible after Selenium login |
| Health smoke test | /health returns 200 |
Access Control (test_oidc_access.py)¶
| Test | Description |
|---|---|
| Unauthenticated redirects | All protected pages/APIs redirect to login without a session |
| user2 portal access | Portal page, keys page, and user API keys accessible |
| user2 admin denied | All admin pages and admin API endpoints return 403 |
| user2 admin write denied | Create backend/profile/policy via admin API returns 403 |
| user3 portal access | Portal and keys pages accessible |
| user3 admin access | All admin pages and admin API endpoints return 200 |
| Policy acceptance access | user2 sees own acceptances; admin list denied; user3 sees all |
Profiles & Policies (test_oidc_profiles.py)¶
| Test | Description |
|---|---|
| Config profile exists | internal-standard profile exists with correct structure |
| Profile OIDC groups | Profile has gasket-users in oidc_groups (returned as list) |
| Profile backends/policies | Profile references ollama-internal and acceptable-use |
| Admin write access | user2 denied CRUD on profiles; user3 can read single profile |
| Config read-only | Config-sourced profiles cannot be modified or deleted by admin |
| Policy visibility | Config policy exists with content and version |
| Policy acceptance flow | user2 can check, accept, and verify policy acceptance status |
| Admin acceptance view | user3 can view user2's acceptances via admin endpoint |
| User API key create | Key creation gated by policy acceptance; revealed key has gsk_ prefix |
| Key visibility | Created keys appear in user's list and admin's list |
| Key revocation | user2 can revoke own key; admin can revoke user2's key |
| Cross-user isolation | user2 cannot reveal or revoke user3's keys (404) |
| Backend admin access | user2 denied backend CRUD; user3 can list and read |
Portal (test_oidc_portal.py)¶
| Test | Description |
|---|---|
| User profiles API | user2 and user3 can list profiles via /api/profiles (group-filtered) |
| Profile visibility | user2 sees internal-standard profile (scoped to gasket-users) |
| Profile fields | User-facing profiles include all fields needed by the portal UI |
| Profile groups/backends/policies | Profile correctly references gasket-users, ollama-internal, acceptable-use |
| User policy API | user2 and user3 can read policy details via /api/policies/<id> |
| Policy content | Policy has name, content, and version for user review |
| Unauthenticated denied | Profile and policy endpoints redirect unauthenticated users to login |
| Portal renders profiles | Browser-level: user2's portal page renders profile cards (not empty) |
| Portal shows profile name | Browser-level: internal-standard appears in the rendered portal page |
| Admin link visibility | Browser-level: user3 sees Admin link in navbar; user2 does not |
Test Results & Logs¶
Both scripts write output to directories with .gitignore files:
| Directory | Contents |
|---|---|
test-results/ |
JUnit XML, HTML reports, failure screenshots (.png) |
logs/ |
access.log (HTTP requests), error.log (app errors), gasket.log (app-level logs) |
Screenshots are only captured when Selenium tests fail — if all tests pass, there will be no .png files.
Test Users¶
These users are created by provision.sh:
| User | Password | App Token | Groups | Access |
|---|---|---|---|---|
| user1 | password |
— | test-users | Open WebUI only (negative test) |
| user2 | password |
user2-app-password |
test-users, gasket-users | Portal |
| user3 | password |
user3-app-password |
test-users, gasket-users, gasket-admins | Portal + Admin |
App password tokens are used for ROPC token exchange (Authentik requires these instead of regular passwords for the client_credentials grant).
Docker Compose Files¶
| File | Purpose |
|---|---|
docker-compose-dev.yaml |
Base dev services (postgres + gasket) |
docker-compose-test.yaml |
Override: adds GASKET_TEST_MODE=1 to gasket |
docker-compose-test-runner.yaml |
Standalone test-runner with Chromium (no test mode) |
Dockerfile.test |
Test runner image (python + chromium + pytest + selenium) |
test.sh uses: docker-compose-dev.yaml + docker-compose-test.yaml
test-oidc.sh uses: docker-compose-dev.yaml + docker-compose-test-runner.yaml
Writing New Tests¶
General Tests¶
Tests are organised into modules under tests/:
| Directory | Contents |
|---|---|
tests/ |
Top-level tests (health, portal, backends, profiles, etc) |
tests/api_keys/ |
API key tests (create, read, edit, revoke, admin, etc) |
tests/policies/ |
Policy tests (CRUD, acceptance, config policies) |
tests/oidc/ |
OIDC flow tests (separate runner, see below) |
All general tests automatically get the test bypass session (user3/admin).
# tests/test_example.py
class TestExample:
def test_something(self, client):
response = client.get("/some-endpoint")
assert response.status_code == 200
The client fixture is provided by tests/conftest.py. Sub-directories like tests/api_keys/ have their own conftest.py with additional fixtures (e.g. helper functions to create keys, accept policies).
OIDC Tests¶
Add test files to tests/oidc/. These run against a live environment.
# tests/oidc/test_example.py
from .conftest import GASKET_URL
class TestExample:
def test_browser_check(self, browser, gasket_url):
browser.get(f"{gasket_url}/some-page")
assert "expected" in browser.page_source
def test_api_as_user2(self, user2_session, gasket_url):
resp = user2_session.get(f"{gasket_url}/api/keys")
assert resp.status_code == 200
def test_api_as_admin(self, user3_session, gasket_url):
resp = user3_session.get(f"{gasket_url}/admin/api/profiles")
assert resp.status_code == 200
def test_unauthenticated(self, anon_session, gasket_url):
resp = anon_session.get(f"{gasket_url}/admin", allow_redirects=True)
assert "login" in resp.url.lower()
Available fixtures from tests/oidc/conftest.py:
gasket_url— base URL for the Gasket portalbrowser— headless Chromium WebDriveruser2_token— ROPC tokens for user2user3_token— ROPC tokens for user3user2_session—requests.Sessionauthenticated as user2 (session scope)user3_session—requests.Sessionauthenticated as user3 (session scope)anon_session— unauthenticatedrequests.Session
The session-based fixtures log in via Selenium once per test run and extract the Flask session cookies for use in requests.Session. This allows API-level access testing without needing the browser for every request.