Merge pull request #205 from ipnet-mesh/chore/hide-test-users

feat: hide users with test OIDC role from public views
This commit is contained in:
JingleManSweep
2026-05-09 00:33:24 +01:00
committed by GitHub
12 changed files with 526 additions and 16 deletions
+4
View File
@@ -390,6 +390,10 @@ WEB_PORT=8080
# Default: member
# OIDC_ROLE_MEMBER=member
# IdP role name for test users (excluded from public member views and counts)
# Default: test
# OIDC_ROLE_TEST=test
# Secret for signing session cookies (required when OIDC_ENABLED=true)
# Generate with: openssl rand -hex 32
# OIDC_SESSION_SECRET=
+1
View File
@@ -658,6 +658,7 @@ Key variables:
- `OIDC_ROLE_ADMIN` - IdP role name for admin access (default: `admin`)
- `OIDC_ROLE_OPERATOR` - IdP role name for operator access (default: `operator`)
- `OIDC_ROLE_MEMBER` - IdP role name for member access (default: `member`)
- `OIDC_ROLE_TEST` - IdP role name for test users, excluded from public views (default: `test`)
- `OIDC_SESSION_SECRET` - Secret for signing session cookies (required if OIDC_ENABLED=true)
- `OIDC_SESSION_MAX_AGE` - Session lifetime in seconds (default: `86400`)
- `OIDC_COOKIE_SECURE` - HTTPS-only cookies (default: `false`)
@@ -0,0 +1,225 @@
# Plan: Hide Users with "test" OIDC Role
**Date:** 2025-05-08
**Status:** Draft
## Problem
Users with a `test` OIDC role should be completely hidden from the Members page and excluded from all member/operator counts across the UI. This applies regardless of whether the user also holds `member` or `operator` roles. Currently, there is no mechanism to filter out test users — they appear alongside real members and operators.
## Current Behavior
### How Roles Work
- OIDC roles are stored as a **comma-separated string** in `user_profiles.roles` (e.g., `"member,test"`, `"operator,member,test"`)
- Roles are synced from the IdP on every authenticated request via the `X-User-Roles` header
- Role names are configurable via env vars (`OIDC_ROLE_ADMIN`, `OIDC_ROLE_OPERATOR`, `OIDC_ROLE_MEMBER`) but default to `"admin"`, `"operator"`, `"member"`
### Where Member/Operator Data is Displayed
There are **three surfaces** that show user data or counts:
1. **Homepage stats panel** (`home.js``renderMembersPanel()`) — shows `total_operators` and `total_members` from the `/api/v1/dashboard/stats` endpoint
2. **Members page** (`members.js`) — fetches all profiles from `/api/v1/user/profiles`, then client-side filters into "Operators" and "Members" groups
3. **Dashboard stats API** (`dashboard.py``get_stats()`) — counts profiles where `roles.contains("operator")` and `roles.contains("member")` independently
### Existing Discrepancy (pre-existing bug)
The server-side counts (`dashboard.py`) count a user with both `operator` and `member` in **both** totals. The client-side Members page excludes operators from the "Members" group. This means homepage totals can be higher than what the Members page shows. This plan does not fix that bug but should not make it worse.
## Approach
Add a configurable "test role" that, when present on a user profile, excludes that user from all public-facing member displays and counts. The filtering should happen at the **API level** so both the homepage stats and the Members page list are consistent.
### New Configuration
| Variable | Description | Default |
|----------|-------------|---------|
| `OIDC_ROLE_TEST` | IdP role name that marks a user as a test user | `test` |
This follows the existing pattern of `OIDC_ROLE_ADMIN`, `OIDC_ROLE_OPERATOR`, `OIDC_ROLE_MEMBER`.
### Scope of Changes
#### 1. Configuration (`common/config.py`)
- Add `oidc_role_test: str = Field(default="test", ...)` to `WebSettings`
- Add `test_role` to the `role_names` dict in `_build_config_json()` (in `web/app.py`) so the frontend knows which role marks test users
- Update `AGENTS.md` and `.env.example` with the new env var
#### 2. Dashboard Stats API (`api/routes/dashboard.py`)
**File:** `src/meshcore_hub/api/routes/dashboard.py` (~lines 209-246)
Read `test_role` from config using the existing `get_web_settings()` pattern (already imported at line 209). Guard against empty string to prevent accidental universal exclusion.
Add a `test_role` variable alongside `operator_role` and `member_role`:
```python
test_role = web_settings.oidc_role_test
```
Modify the `total_operators` and `total_members` queries to **exclude** profiles whose `roles` column contains the test role string. Only apply the filter when `test_role` is non-empty:
```python
# Current:
total_operators = ... .where(UserProfile.roles.contains(operator_role))
# New:
total_operators = (select(func.count()).select_from(UserProfile)
.where(UserProfile.roles.contains(operator_role)))
if test_role:
total_operators = total_operators.where(~UserProfile.roles.contains(test_role))
```
Same pattern for `total_members`. This ensures the homepage stats panel shows correct counts.
> **Note on `contains()`**: Since `roles` is a comma-separated string (e.g., `"operator,member,test"`), `contains("test")` will correctly match. A false positive could theoretically occur if a role name is a substring of another (e.g., `"test"` matching `"contest"`), but this is the same approach already used for operator/member counts and is acceptable given role names are short, well-known strings.
>
> **Guard condition**: If `OIDC_ROLE_TEST` is set to an empty string, the filter is skipped entirely (no profiles excluded). This allows disabling the feature without removing the config variable.
#### 3. User Profiles List API (`api/routes/user_profiles.py`)
**File:** `src/meshcore_hub/api/routes/user_profiles.py`
The `GET /api/v1/user/profiles` endpoint returns all profiles. Add an optional query parameter or internal filter to exclude test users from the response. Two options:
- **Option A (recommended):** Add a `?exclude_test=true` query parameter (default `true`) that filters out profiles with the test role. This keeps the API general-purpose while defaulting to the desired behavior.
- **Option B:** Always exclude test users from the list endpoint. Simpler but less flexible.
**Chosen: Option A** — default to excluding test users but allow explicit opt-out.
Get the `test_role` string from config using the same `get_web_settings()` pattern as `dashboard.py`. Apply the filter only when both `exclude_test=true` and `test_role` is non-empty (guard against empty string matching all rows).
Implementation:
```python
from meshcore_hub.common.config import get_web_settings
@router.get("", response_model=UserProfileList)
async def list_profiles(
db: Annotated[AsyncSession, Depends(get_db)],
_: Annotated[None, Depends(require_read)],
exclude_test: bool = Query(default=True),
limit: int = Query(default=50, le=500),
offset: int = Query(default=0, ge=0),
) -> UserProfileList:
web_settings = get_web_settings()
test_role = web_settings.oidc_role_test
count_query = select(func.count(UserProfile.id))
if exclude_test and test_role:
count_query = count_query.where(~UserProfile.roles.contains(test_role))
total = session.execute(count_query).scalar() or 0
query = (
select(UserProfile)
.options(
selectinload(UserProfile.node_associations).selectinload(
UserProfileNode.node
)
)
.order_by(UserProfile.name)
.offset(offset)
.limit(limit)
)
if exclude_test and test_role:
query = query.where(~UserProfile.roles.contains(test_role))
# ... rest unchanged ...
```
The `total` in the paginated response must also exclude test users when `exclude_test=true`.
#### 4. Members Page Frontend (`web/static/js/spa/pages/members.js`)
**File:** `src/meshcore_hub/web/static/js/spa/pages/members.js`
The Members page already fetches profiles and does client-side filtering into operator/member groups. After the API change (step 3), test users will already be excluded from the API response by default.
However, as a **defense-in-depth** measure, also filter client-side using the `role_names.test` from the frontend config:
```javascript
const testRole = config.role_names.test;
const realProfiles = profiles.filter(p => !p.roles || !p.roles.includes(testRole));
```
This ensures test users are never displayed even if the API defaults change.
Update the Members page total stats (the "X Operators, Y Members" summary) to use the already-filtered lists.
#### 5. Homepage Frontend (`web/static/js/spa/pages/home.js`)
**File:** `src/meshcore_hub/web/static/js/spa/pages/home.js` (~lines 169-194)
The homepage stats panel (`renderMembersPanel()`) reads `total_operators` and `total_members` from the dashboard stats API. After step 2, these counts will already exclude test users. **No frontend changes needed** for the homepage.
#### 6. Profile Page (`web/static/js/spa/pages/profile.js`)
No changes needed — individual profile pages should still be accessible via direct URL. Test users are only hidden from aggregate views (lists and counts), not from their own profile page.
#### 7. Frontend Config (`web/app.py`)
**File:** `src/meshcore_hub/web/app.py` (`_build_config_json()`)
Add `test` to the `role_names` dict:
```python
role_names = {
"admin": app.state.oidc_role_admin,
"operator": app.state.oidc_role_operator,
"member": app.state.oidc_role_member,
"test": app.state.oidc_role_test, # NEW
}
```
#### 8. App State Initialization (`web/app.py`)
Ensure `oidc_role_test` is stored in `app.state` alongside the existing role settings, so it's available to route handlers and the config builder.
### Files Changed (Summary)
| File | Change |
|------|--------|
| `src/meshcore_hub/common/config.py` | Add `oidc_role_test` setting |
| `src/meshcore_hub/api/routes/dashboard.py` | Exclude test users from operator/member count queries |
| `src/meshcore_hub/api/routes/user_profiles.py` | Add `exclude_test` query param, filter test users from list |
| `src/meshcore_hub/web/app.py` | Add `test` to `role_names` config; store `oidc_role_test` in app state |
| `src/meshcore_hub/web/static/js/spa/pages/members.js` | Client-side defense-in-depth filter for test role |
| `AGENTS.md` | Document `OIDC_ROLE_TEST` env var |
| `.env.example` | Add `OIDC_ROLE_TEST` entry |
### Tests to Add/Update
| Test File | Change |
|-----------|--------|
| `tests/test_api/test_dashboard.py` | Add test: users with test role excluded from counts |
| `tests/test_api/test_user_profiles.py` | Add test: `exclude_test=true` filters test users from list; verify `total` count is correct |
| `tests/test_web/` | Verify frontend config includes `role_names.test` |
### Edge Cases
- **User with `operator,test` roles**: Should be excluded from both the Operators section and the operator count
- **User with `member,test` roles**: Should be excluded from both the Members section and the member count
- **User with `operator,member,test` roles**: Should be excluded from everything
- **Test user accessing their own profile**: Should still work — they can view and edit their own profile
- **Test user adopting nodes**: Should still work — adoption is a separate concern from display
- **`OIDC_ROLE_TEST` not configured**: Defaults to `"test"`, matching the common convention
- **No users have the test role**: Behavior is identical to current — no regressions
### Out of Scope
- Fixing the pre-existing discrepancy between server-side and client-side member counts (operators counted in both server-side totals)
- Hiding test users from the admin API or database — they should still exist and be manageable
- Revoking test user permissions or access — they should still be able to log in and use the dashboard
- Migration: No schema changes needed — the `roles` column already stores arbitrary comma-separated role strings
## Implementation Order
1. Add `oidc_role_test` to `WebSettings` in `config.py`
2. Wire `oidc_role_test` into `app.state` and `_build_config_json()` in `web/app.py`
3. Update dashboard stats queries in `api/routes/dashboard.py`
4. Add `exclude_test` filter to `GET /api/v1/user/profiles` in `api/routes/user_profiles.py`
5. Update Members page client-side filter in `members.js`
6. Add/update tests
7. Update documentation (`AGENTS.md`, `.env.example`)
8. Run `pre-commit run --all-files` and `pytest`
@@ -0,0 +1,53 @@
# Tasks: Hide Users with "test" OIDC Role
## Implementation
- [ ] **T1: Add `oidc_role_test` to `WebSettings`** (`src/meshcore_hub/common/config.py`)
- Add `oidc_role_test: str = Field(default="test", description="IdP role name for test users")` to `WebSettings`
- Follow existing pattern of `oidc_role_admin`, `oidc_role_operator`, `oidc_role_member`
- [ ] **T2: Wire `oidc_role_test` into `app.state`** (`src/meshcore_hub/web/app.py`)
- Add `app.state.oidc_role_test = settings.oidc_role_test` in both OIDC-enabled branch (~line 418-420) and OIDC-disabled branch (~line 423-425)
- Add `"test": app.state.oidc_role_test` to `role_names` dict in `_build_config_json()` (~line 241)
- [ ] **T3: Exclude test users from dashboard stats** (`src/meshcore_hub/api/routes/dashboard.py`)
- In `get_stats()`, read `test_role = get_web_settings().oidc_role_test` alongside existing `operator_role`/`member_role`
- Add `~UserProfile.roles.contains(test_role)` filter to `total_operators` and `total_members` count queries
- Guard: only apply filter when `test_role` is non-empty (`if test_role:`)
- [ ] **T4: Add `exclude_test` filter to profiles list endpoint** (`src/meshcore_hub/api/routes/user_profiles.py`)
- Add `exclude_test: bool = Query(default=True)` parameter to `list_profiles()`
- Import `get_web_settings` from config
- Read `test_role = get_web_settings().oidc_role_test`
- Filter both count query and data query: `if exclude_test and test_role: query = query.where(~UserProfile.roles.contains(test_role))`
- [ ] **T5: Client-side defense-in-depth filter in Members page** (`src/meshcore_hub/web/static/js/spa/pages/members.js`)
- Import `config` from `../app.js` (or access via existing pattern)
- Filter out profiles where `p.roles` includes `config.role_names.test`
- Use filtered list for rendering and total badge
## Tests
- [ ] **T6: Dashboard stats tests** (`tests/test_api/test_dashboard.py`)
- Test: users with test role excluded from `total_operators` and `total_members`
- Test: users without test role still counted normally
- Test: empty `oidc_role_test` does not filter any users
- [ ] **T7: User profiles list tests** (`tests/test_api/test_user_profiles.py`)
- Test: `exclude_test=true` (default) filters test users from list
- Test: `exclude_test=false` includes test users
- Test: `total` count in paginated response excludes test users
- Test: empty `oidc_role_test` does not filter
- [ ] **T8: Frontend config test** (`tests/test_web/`)
- Test: `/api/v1/web/config` response includes `role_names.test`
## Documentation & Quality
- [ ] **T9: Update documentation** (`AGENTS.md`, `.env.example`)
- Add `OIDC_ROLE_TEST` to environment variables table in `AGENTS.md`
- Add `OIDC_ROLE_TEST` entry to `.env.example`
- [ ] **T10: Run quality checks**
- `pre-commit run --all-files`
- `pytest tests/test_api/test_dashboard.py tests/test_api/test_user_profiles.py`
+19 -14
View File
@@ -211,24 +211,29 @@ async def get_stats(
web_settings = get_web_settings()
operator_role = web_settings.oidc_role_operator
member_role = web_settings.oidc_role_member
test_role = web_settings.oidc_role_test
total_operators = (
session.execute(
select(func.count())
.select_from(UserProfile)
.where(UserProfile.roles.contains(operator_role))
).scalar()
or 0
total_operators_query = (
select(func.count())
.select_from(UserProfile)
.where(UserProfile.roles.contains(operator_role))
)
if test_role:
total_operators_query = total_operators_query.where(
~UserProfile.roles.contains(test_role)
)
total_operators = session.execute(total_operators_query).scalar() or 0
total_members = (
session.execute(
select(func.count())
.select_from(UserProfile)
.where(UserProfile.roles.contains(member_role))
).scalar()
or 0
total_members_query = (
select(func.count())
.select_from(UserProfile)
.where(UserProfile.roles.contains(member_role))
)
if test_role:
total_members_query = total_members_query.where(
~UserProfile.roles.contains(test_role)
)
total_members = session.execute(total_members_query).scalar() or 0
return DashboardStats(
total_nodes=total_nodes,
+22 -1
View File
@@ -4,12 +4,13 @@ import logging
from fastapi import APIRouter, HTTPException, Query, Request, status
from pydantic import AnyUrl
from sqlalchemy import func, select
from sqlalchemy import func, or_, select
from sqlalchemy.orm import selectinload
from meshcore_hub.api.auth import RequireRead, RequireUserOwner, X_USER_ID_HEADER
from meshcore_hub.api.dependencies import DbSession
from meshcore_hub.api.profile_utils import get_or_create_profile
from meshcore_hub.common.config import get_web_settings
from meshcore_hub.common.models import UserProfile
from meshcore_hub.common.models.user_profile_node import UserProfileNode
from meshcore_hub.common.schemas.user_profiles import (
@@ -46,11 +47,24 @@ def _build_adopted_nodes(profile: UserProfile) -> list[AdoptedNodeRead]:
async def list_profiles(
_: RequireRead,
session: DbSession,
exclude_test: bool = Query(
default=True, description="Exclude test users from results"
),
limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0),
) -> UserProfileList:
"""List all user profiles with node counts. No user_id exposed."""
web_settings = get_web_settings()
test_role = web_settings.oidc_role_test
count_query = select(func.count(UserProfile.id))
if exclude_test and test_role:
count_query = count_query.where(
or_(
UserProfile.roles.is_(None),
~UserProfile.roles.contains(test_role),
)
)
total = session.execute(count_query).scalar() or 0
query = (
@@ -64,6 +78,13 @@ async def list_profiles(
.offset(offset)
.limit(limit)
)
if exclude_test and test_role:
query = query.where(
or_(
UserProfile.roles.is_(None),
~UserProfile.roles.contains(test_role),
)
)
profiles = session.execute(query).scalars().all()
items = []
+4
View File
@@ -311,6 +311,10 @@ class WebSettings(CommonSettings):
oidc_role_member: str = Field(
default="member", description="IdP role name for member access"
)
oidc_role_test: str = Field(
default="test",
description="IdP role name for test users (excluded from public views)",
)
oidc_session_secret: Optional[str] = Field(
default=None, description="Secret key for signing session cookies"
)
+3
View File
@@ -310,6 +310,7 @@ def _build_config_json(app: FastAPI, request: Request) -> str:
"admin": app.state.oidc_role_admin,
"operator": app.state.oidc_role_operator,
"member": app.state.oidc_role_member,
"test": app.state.oidc_role_test,
}
if getattr(app.state, "oidc_enabled", False):
@@ -418,11 +419,13 @@ def create_app(
app.state.oidc_role_admin = settings.oidc_role_admin
app.state.oidc_role_operator = settings.oidc_role_operator
app.state.oidc_role_member = settings.oidc_role_member
app.state.oidc_role_test = settings.oidc_role_test
else:
app.state.oidc_enabled = False
app.state.oidc_role_admin = settings.oidc_role_admin
app.state.oidc_role_operator = settings.oidc_role_operator
app.state.oidc_role_member = settings.oidc_role_member
app.state.oidc_role_test = settings.oidc_role_test
app.state.endpoint_access = _build_endpoint_access(
role_admin=settings.oidc_role_admin,
@@ -69,9 +69,12 @@ export async function render(container, params, router) {
const roleNames = config.role_names || {};
const operatorRole = roleNames.operator || 'operator';
const memberRole = roleNames.member || 'member';
const testRole = roleNames.test || 'test';
const resp = await apiGet('/api/v1/user/profiles', { limit: 500 });
const profiles = resp.items || [];
const allProfiles = resp.items || [];
const profiles = allProfiles.filter(p => !p.roles || !p.roles.includes(testRole));
if (profiles.length === 0) {
litRender(html`
+69
View File
@@ -1,10 +1,12 @@
"""Tests for dashboard API routes."""
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
import pytest
from meshcore_hub.common.models import Advertisement, Message, Node
from meshcore_hub.common.models import UserProfile
class TestDashboardStats:
@@ -282,3 +284,70 @@ class TestNodeCountHistory:
# At least one day should have a count > 0 (cumulative)
# The last day should have count >= 1
assert data["data"][-1]["count"] >= 1
class TestDashboardTestUserExclusion:
"""Tests for test user exclusion from dashboard stats."""
@pytest.fixture
def profiles_with_roles(self, api_db_session):
"""Create profiles with various role combinations."""
profiles = []
for user_id, name, roles in [
("op-1", "Operator One", "operator"),
("op-2", "Operator Two", "operator,member"),
("mem-1", "Member One", "member"),
("test-1", "Test Operator", "operator,test"),
("test-2", "Test Member", "member,test"),
("test-3", "Test Both", "operator,member,test"),
("none-1", "No Roles", ""),
]:
p = UserProfile(user_id=user_id, name=name, roles=roles)
api_db_session.add(p)
profiles.append((user_id, roles))
api_db_session.commit()
return profiles
def test_test_users_excluded_from_operator_count(
self, client_no_auth, profiles_with_roles
):
"""Test that users with the test role are excluded from operator count."""
with patch("meshcore_hub.common.config.get_web_settings") as mock_settings:
settings = mock_settings.return_value
settings.oidc_role_operator = "operator"
settings.oidc_role_member = "member"
settings.oidc_role_test = "test"
response = client_no_auth.get("/api/v1/dashboard/stats")
assert response.status_code == 200
data = response.json()
assert data["total_operators"] == 2
assert data["total_members"] == 2
def test_empty_test_role_excludes_no_one(self, client_no_auth, profiles_with_roles):
"""Test that an empty test role does not filter any users."""
with patch("meshcore_hub.common.config.get_web_settings") as mock_settings:
settings = mock_settings.return_value
settings.oidc_role_operator = "operator"
settings.oidc_role_member = "member"
settings.oidc_role_test = ""
response = client_no_auth.get("/api/v1/dashboard/stats")
assert response.status_code == 200
data = response.json()
assert data["total_operators"] == 4
assert data["total_members"] == 4
def test_no_profiles(self, client_no_auth):
"""Test stats with no profiles returns zero counts."""
with patch("meshcore_hub.common.config.get_web_settings") as mock_settings:
settings = mock_settings.return_value
settings.oidc_role_operator = "operator"
settings.oidc_role_member = "member"
settings.oidc_role_test = "test"
response = client_no_auth.get("/api/v1/dashboard/stats")
assert response.status_code == 200
data = response.json()
assert data["total_operators"] == 0
assert data["total_members"] == 0
+103
View File
@@ -1,5 +1,11 @@
"""Tests for user profile API routes."""
from unittest.mock import patch
import pytest
from meshcore_hub.common.models import UserProfile
TEST_USER_ID = "oidc-user-123"
OTHER_USER_ID = "oidc-user-456"
USER_HEADERS = {"X-User-Id": TEST_USER_ID, "X-User-Roles": "operator"}
@@ -351,3 +357,100 @@ class TestUpdateProfile:
assert response.status_code == 200
data = response.json()
assert data["callsign"] == "NR1"
class TestListProfilesExcludeTest:
"""Tests for exclude_test query parameter on GET /user/profiles."""
@pytest.fixture
def profiles_with_test_role(self, api_db_session):
"""Create profiles including some with the test role."""
profiles = []
for user_id, name, roles in [
("real-op", "Real Operator", "operator"),
("real-mem", "Real Member", "member"),
("test-op", "Test Operator", "operator,test"),
("test-mem", "Test Member", "member,test"),
]:
p = UserProfile(user_id=user_id, name=name, roles=roles)
api_db_session.add(p)
profiles.append(p)
api_db_session.commit()
return profiles
def test_exclude_test_true_filters_test_users(
self, client_no_auth, profiles_with_test_role
):
"""Test that exclude_test=true (default) filters test users."""
with patch(
"meshcore_hub.api.routes.user_profiles.get_web_settings"
) as mock_settings:
settings = mock_settings.return_value
settings.oidc_role_test = "test"
response = client_no_auth.get(
"/api/v1/user/profiles?exclude_test=true",
headers=USER_HEADERS,
)
assert response.status_code == 200
data = response.json()
names = [item["name"] for item in data["items"]]
assert "Real Operator" in names
assert "Real Member" in names
assert "Test Operator" not in names
assert "Test Member" not in names
assert data["total"] == 2
def test_exclude_test_default_is_true(
self, client_no_auth, profiles_with_test_role
):
"""Test that exclude_test defaults to true."""
with patch(
"meshcore_hub.api.routes.user_profiles.get_web_settings"
) as mock_settings:
settings = mock_settings.return_value
settings.oidc_role_test = "test"
response = client_no_auth.get(
"/api/v1/user/profiles",
headers=USER_HEADERS,
)
assert response.status_code == 200
data = response.json()
assert data["total"] == 2
def test_exclude_test_false_includes_test_users(
self, client_no_auth, profiles_with_test_role
):
"""Test that exclude_test=false includes test users."""
with patch(
"meshcore_hub.api.routes.user_profiles.get_web_settings"
) as mock_settings:
settings = mock_settings.return_value
settings.oidc_role_test = "test"
response = client_no_auth.get(
"/api/v1/user/profiles?exclude_test=false",
headers=USER_HEADERS,
)
assert response.status_code == 200
data = response.json()
assert data["total"] == 4
def test_empty_test_role_does_not_filter(
self, client_no_auth, profiles_with_test_role
):
"""Test that an empty test role does not filter any users."""
with patch(
"meshcore_hub.api.routes.user_profiles.get_web_settings"
) as mock_settings:
settings = mock_settings.return_value
settings.oidc_role_test = ""
response = client_no_auth.get(
"/api/v1/user/profiles?exclude_test=true",
headers=USER_HEADERS,
)
assert response.status_code == 200
data = response.json()
assert data["total"] == 4
+19
View File
@@ -149,6 +149,25 @@ class TestConfigJsonXssEscaping:
assert parsed["network_name"] == "Test Network"
assert parsed["network_city"] == "Test City"
def test_build_config_json_includes_test_role_name(self, web_app: Any) -> None:
"""_build_config_json includes role_names.test in the config."""
from starlette.requests import Request
scope = {
"type": "http",
"method": "GET",
"path": "/",
"query_string": b"",
"headers": [],
}
request = Request(scope)
result = _build_config_json(web_app, request)
parsed = json.loads(result)
assert "role_names" in parsed
assert "test" in parsed["role_names"]
assert parsed["role_names"]["test"] == "test"
class TestCheckApiAccess:
"""Unit tests for check_api_access with _OPEN, _AUTHENTICATED, and role-based levels."""