mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-06-26 21:12:22 +02:00
31418e6847
Move adopt/release from profile routes to dedicated /v1/adoptions endpoint. Node API now returns adopted_by field. Profile page shows read-only adopted nodes. Node detail page has adopt/release buttons (operator adopts, admin can release any). Admin release bypasses ownership check.
155 lines
5.7 KiB
Python
155 lines
5.7 KiB
Python
"""Tests for user profile API routes."""
|
|
|
|
TEST_USER_ID = "oidc-user-123"
|
|
OTHER_USER_ID = "oidc-user-456"
|
|
USER_HEADERS = {"X-User-Id": TEST_USER_ID, "X-User-Roles": "operator"}
|
|
OTHER_USER_HEADERS = {"X-User-Id": OTHER_USER_ID, "X-User-Roles": "operator"}
|
|
OPERATOR_HEADERS = {
|
|
"X-User-Id": TEST_USER_ID,
|
|
"X-User-Roles": "operator",
|
|
}
|
|
MEMBER_ONLY_HEADERS = {
|
|
"X-User-Id": TEST_USER_ID,
|
|
"X-User-Roles": "member",
|
|
}
|
|
NO_ROLES_HEADERS = {
|
|
"X-User-Id": TEST_USER_ID,
|
|
"X-User-Roles": "",
|
|
}
|
|
|
|
|
|
class TestGetProfile:
|
|
"""Tests for GET /user/profile/{user_id} endpoint."""
|
|
|
|
def test_get_profile_auto_creates(self, client_no_auth):
|
|
"""Test getting a non-existent profile auto-creates it."""
|
|
response = client_no_auth.get(
|
|
f"/api/v1/user/profile/{TEST_USER_ID}",
|
|
headers=USER_HEADERS,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["user_id"] == TEST_USER_ID
|
|
assert data["name"] is None
|
|
assert data["callsign"] is None
|
|
assert "id" in data
|
|
assert "created_at" in data
|
|
assert data["nodes"] == []
|
|
|
|
def test_get_existing_profile(self, client_no_auth, sample_user_profile):
|
|
"""Test getting an existing profile."""
|
|
response = client_no_auth.get(
|
|
f"/api/v1/user/profile/{sample_user_profile.user_id}",
|
|
headers=USER_HEADERS,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["user_id"] == sample_user_profile.user_id
|
|
assert data["name"] == sample_user_profile.name
|
|
assert data["callsign"] == sample_user_profile.callsign
|
|
|
|
def test_get_profile_with_adopted_nodes(
|
|
self, client_no_auth, sample_user_profile, sample_adopted_node
|
|
):
|
|
"""Test profile includes adopted nodes."""
|
|
response = client_no_auth.get(
|
|
f"/api/v1/user/profile/{sample_user_profile.user_id}",
|
|
headers=USER_HEADERS,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["nodes"]) == 1
|
|
assert data["nodes"][0]["public_key"] == "abc123def456abc123def456abc123de"
|
|
assert "adopted_at" in data["nodes"][0]
|
|
|
|
def test_get_profile_rejects_wrong_user(self, client_no_auth):
|
|
"""Test that a user cannot access another user's profile."""
|
|
response = client_no_auth.get(
|
|
f"/api/v1/user/profile/{TEST_USER_ID}",
|
|
headers=OTHER_USER_HEADERS,
|
|
)
|
|
assert response.status_code == 403
|
|
assert "access denied" in response.json()["detail"].lower()
|
|
|
|
def test_get_profile_rejects_missing_user_id(self, client_no_auth):
|
|
"""Test that missing X-User-Id header is rejected."""
|
|
response = client_no_auth.get(
|
|
f"/api/v1/user/profile/{TEST_USER_ID}",
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
def test_get_profile_requires_auth(self, client_with_auth):
|
|
"""Test getting profile requires auth when keys configured."""
|
|
response = client_with_auth.get(
|
|
f"/api/v1/user/profile/{TEST_USER_ID}",
|
|
headers=USER_HEADERS,
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
response = client_with_auth.get(
|
|
f"/api/v1/user/profile/{TEST_USER_ID}",
|
|
headers={
|
|
**USER_HEADERS,
|
|
"Authorization": "Bearer test-read-key",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestUpdateProfile:
|
|
"""Tests for PUT /user/profile/{user_id} endpoint."""
|
|
|
|
def test_update_profile_name(self, client_no_auth, sample_user_profile):
|
|
"""Test updating profile name."""
|
|
response = client_no_auth.put(
|
|
f"/api/v1/user/profile/{sample_user_profile.user_id}",
|
|
json={"name": "New Name"},
|
|
headers=USER_HEADERS,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["name"] == "New Name"
|
|
assert data["callsign"] == sample_user_profile.callsign
|
|
|
|
def test_update_profile_callsign(self, client_no_auth, sample_user_profile):
|
|
"""Test updating profile callsign."""
|
|
response = client_no_auth.put(
|
|
f"/api/v1/user/profile/{sample_user_profile.user_id}",
|
|
json={"callsign": "G1NEW"},
|
|
headers=USER_HEADERS,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["callsign"] == "G1NEW"
|
|
assert data["name"] == sample_user_profile.name
|
|
|
|
def test_update_profile_auto_creates(self, client_no_auth):
|
|
"""Test updating a non-existent profile auto-creates it."""
|
|
response = client_no_auth.put(
|
|
f"/api/v1/user/profile/{TEST_USER_ID}",
|
|
json={"name": "Auto Created", "callsign": "W1AUTO"},
|
|
headers=USER_HEADERS,
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["user_id"] == TEST_USER_ID
|
|
assert data["name"] == "Auto Created"
|
|
assert data["callsign"] == "W1AUTO"
|
|
|
|
def test_update_profile_rejects_wrong_user(self, client_no_auth):
|
|
"""Test that a user cannot update another user's profile."""
|
|
response = client_no_auth.put(
|
|
f"/api/v1/user/profile/{TEST_USER_ID}",
|
|
json={"name": "Hacked"},
|
|
headers=OTHER_USER_HEADERS,
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
def test_update_profile_rejects_missing_user_id(self, client_no_auth):
|
|
"""Test that missing X-User-Id header is rejected."""
|
|
response = client_no_auth.put(
|
|
f"/api/v1/user/profile/{TEST_USER_ID}",
|
|
json={"name": "No Auth"},
|
|
)
|
|
assert response.status_code == 401
|