mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-06-11 00:34:54 +02:00
38c792196f
Proxy now injects X-User-Name header from session. Profile auto-creation uses it as the initial name value. Existing profile names are never overwritten.
276 lines
9.3 KiB
Python
276 lines
9.3 KiB
Python
"""Tests for node adoption API routes."""
|
|
|
|
from meshcore_hub.common.models import UserProfile, UserProfileNode
|
|
|
|
TEST_USER_ID = "oidc-user-123"
|
|
OTHER_USER_ID = "oidc-user-456"
|
|
OPERATOR_HEADERS = {
|
|
"X-User-Id": TEST_USER_ID,
|
|
"X-User-Roles": "operator",
|
|
}
|
|
OPERATOR_HEADERS_WITH_NAME = {
|
|
"X-User-Id": TEST_USER_ID,
|
|
"X-User-Roles": "operator",
|
|
"X-User-Name": "Operator Name",
|
|
}
|
|
ADMIN_HEADERS = {
|
|
"X-User-Id": TEST_USER_ID,
|
|
"X-User-Roles": "admin",
|
|
}
|
|
OTHER_OPERATOR_HEADERS = {
|
|
"X-User-Id": OTHER_USER_ID,
|
|
"X-User-Roles": "operator",
|
|
}
|
|
OTHER_ADMIN_HEADERS = {
|
|
"X-User-Id": OTHER_USER_ID,
|
|
"X-User-Roles": "admin",
|
|
}
|
|
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 TestAdoptNode:
|
|
"""Tests for POST /v1/adoptions endpoint."""
|
|
|
|
def test_adopt_node_success(self, client_no_auth, sample_node):
|
|
"""Test adopting a node."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": sample_node.public_key},
|
|
headers=OPERATOR_HEADERS,
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["public_key"] == sample_node.public_key
|
|
assert data["name"] == sample_node.name
|
|
assert "adopted_at" in data
|
|
|
|
def test_adopt_node_auto_creates_profile(self, client_no_auth, sample_node):
|
|
"""Test adopting a node auto-creates the user profile."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": sample_node.public_key},
|
|
headers=OPERATOR_HEADERS,
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
def test_adopt_node_auto_creates_profile_with_name(
|
|
self, client_no_auth, sample_node, api_db_session
|
|
):
|
|
"""Test adopting a node auto-creates profile with IdP name."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": sample_node.public_key},
|
|
headers=OPERATOR_HEADERS_WITH_NAME,
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
from meshcore_hub.common.models import UserProfile as UP
|
|
|
|
profile = (
|
|
api_db_session.query(UP).filter(UP.user_id == TEST_USER_ID).one_or_none()
|
|
)
|
|
assert profile is not None
|
|
assert profile.name == "Operator Name"
|
|
|
|
def test_adopt_node_by_admin(self, client_no_auth, sample_node):
|
|
"""Test an admin can adopt a node."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": sample_node.public_key},
|
|
headers=ADMIN_HEADERS,
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
def test_adopt_node_duplicate(self, client_no_auth, sample_adopted_node):
|
|
"""Test adopting an already-adopted node by the same user fails."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": "abc123def456abc123def456abc123de"},
|
|
headers=OPERATOR_HEADERS,
|
|
)
|
|
assert response.status_code == 409
|
|
assert "already adopted" in response.json()["detail"].lower()
|
|
|
|
def test_adopt_node_already_adopted_by_other(
|
|
self, client_no_auth, api_db_session, sample_node
|
|
):
|
|
"""Test adopting a node adopted by another user fails."""
|
|
other_profile = UserProfile(user_id=OTHER_USER_ID, name="Other")
|
|
api_db_session.add(other_profile)
|
|
api_db_session.commit()
|
|
api_db_session.refresh(other_profile)
|
|
|
|
assoc = UserProfileNode(
|
|
user_profile_id=other_profile.id,
|
|
node_id=sample_node.id,
|
|
)
|
|
api_db_session.add(assoc)
|
|
api_db_session.commit()
|
|
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": sample_node.public_key},
|
|
headers=OPERATOR_HEADERS,
|
|
)
|
|
assert response.status_code == 409
|
|
assert "another user" in response.json()["detail"].lower()
|
|
|
|
def test_adopt_node_not_found(self, client_no_auth):
|
|
"""Test adopting a non-existent node fails."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": "a" * 64},
|
|
headers=OPERATOR_HEADERS,
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
def test_adopt_node_requires_operator_or_admin_role(
|
|
self, client_no_auth, sample_node
|
|
):
|
|
"""Test adopting requires operator or admin role."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": sample_node.public_key},
|
|
headers=MEMBER_ONLY_HEADERS,
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
def test_adopt_node_requires_role_header(self, client_no_auth, sample_node):
|
|
"""Test adopting requires role in X-User-Roles."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": sample_node.public_key},
|
|
headers=NO_ROLES_HEADERS,
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
def test_adopt_node_rejects_missing_user_id(self, client_no_auth, sample_node):
|
|
"""Test adopting without X-User-Id header is rejected."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/adoptions",
|
|
json={"public_key": sample_node.public_key},
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
|
|
class TestReleaseNode:
|
|
"""Tests for DELETE /v1/adoptions/{public_key} endpoint."""
|
|
|
|
def test_release_own_node_success(
|
|
self, client_no_auth, sample_node, sample_adopted_node
|
|
):
|
|
"""Test operator releasing their own adopted node."""
|
|
response = client_no_auth.delete(
|
|
f"/api/v1/adoptions/{sample_node.public_key}",
|
|
headers=OPERATOR_HEADERS,
|
|
)
|
|
assert response.status_code == 204
|
|
|
|
def test_release_node_not_adopted(self, client_no_auth, sample_node):
|
|
"""Test releasing a node that is not adopted."""
|
|
response = client_no_auth.delete(
|
|
f"/api/v1/adoptions/{sample_node.public_key}",
|
|
headers=OPERATOR_HEADERS,
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
def test_release_node_not_found(self, client_no_auth):
|
|
"""Test releasing a non-existent node fails."""
|
|
response = client_no_auth.delete(
|
|
f"/api/v1/adoptions/{'z' * 64}",
|
|
headers=OPERATOR_HEADERS,
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
def test_release_node_requires_operator_or_admin(
|
|
self, client_no_auth, sample_node, sample_adopted_node
|
|
):
|
|
"""Test releasing requires operator or admin role."""
|
|
response = client_no_auth.delete(
|
|
f"/api/v1/adoptions/{sample_node.public_key}",
|
|
headers=MEMBER_ONLY_HEADERS,
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
def test_operator_cannot_release_others_node(
|
|
self, client_no_auth, api_db_session, sample_node, sample_user_profile
|
|
):
|
|
"""Test operator cannot release a node adopted by another user."""
|
|
other_profile = UserProfile(user_id=OTHER_USER_ID, name="Other")
|
|
api_db_session.add(other_profile)
|
|
api_db_session.commit()
|
|
api_db_session.refresh(other_profile)
|
|
|
|
assoc = UserProfileNode(
|
|
user_profile_id=other_profile.id,
|
|
node_id=sample_node.id,
|
|
)
|
|
api_db_session.add(assoc)
|
|
api_db_session.commit()
|
|
|
|
response = client_no_auth.delete(
|
|
f"/api/v1/adoptions/{sample_node.public_key}",
|
|
headers=OPERATOR_HEADERS,
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
def test_admin_can_release_others_node(
|
|
self, client_no_auth, api_db_session, sample_node
|
|
):
|
|
"""Test admin can release a node adopted by another user."""
|
|
other_profile = UserProfile(user_id=OTHER_USER_ID, name="Other")
|
|
api_db_session.add(other_profile)
|
|
api_db_session.commit()
|
|
api_db_session.refresh(other_profile)
|
|
|
|
assoc = UserProfileNode(
|
|
user_profile_id=other_profile.id,
|
|
node_id=sample_node.id,
|
|
)
|
|
api_db_session.add(assoc)
|
|
api_db_session.commit()
|
|
|
|
response = client_no_auth.delete(
|
|
f"/api/v1/adoptions/{sample_node.public_key}",
|
|
headers=ADMIN_HEADERS,
|
|
)
|
|
assert response.status_code == 204
|
|
|
|
def test_admin_can_release_own_node(
|
|
self, client_no_auth, sample_node, api_db_session
|
|
):
|
|
"""Test admin can release their own adopted node."""
|
|
profile = UserProfile(user_id=TEST_USER_ID, name="Admin User")
|
|
api_db_session.add(profile)
|
|
api_db_session.commit()
|
|
api_db_session.refresh(profile)
|
|
|
|
assoc = UserProfileNode(
|
|
user_profile_id=profile.id,
|
|
node_id=sample_node.id,
|
|
)
|
|
api_db_session.add(assoc)
|
|
api_db_session.commit()
|
|
|
|
response = client_no_auth.delete(
|
|
f"/api/v1/adoptions/{sample_node.public_key}",
|
|
headers=ADMIN_HEADERS,
|
|
)
|
|
assert response.status_code == 204
|
|
|
|
def test_release_node_rejects_missing_user_id(
|
|
self, client_no_auth, sample_node, sample_adopted_node
|
|
):
|
|
"""Test releasing without X-User-Id header is rejected."""
|
|
response = client_no_auth.delete(
|
|
f"/api/v1/adoptions/{sample_node.public_key}",
|
|
)
|
|
assert response.status_code == 401
|