mirror of
https://github.com/ipnet-mesh/meshcore-hub.git
synced 2026-03-28 17:42:56 +01:00
286 lines
10 KiB
Python
286 lines
10 KiB
Python
"""Tests for member API routes."""
|
|
|
|
|
|
class TestListMembers:
|
|
"""Tests for GET /members endpoint."""
|
|
|
|
def test_list_members_empty(self, client_no_auth):
|
|
"""Test listing members when database is empty."""
|
|
response = client_no_auth.get("/api/v1/members")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["items"] == []
|
|
assert data["total"] == 0
|
|
|
|
def test_list_members_with_data(self, client_no_auth, sample_member):
|
|
"""Test listing members with data in database."""
|
|
response = client_no_auth.get("/api/v1/members")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["items"]) == 1
|
|
assert data["total"] == 1
|
|
assert data["items"][0]["member_id"] == sample_member.member_id
|
|
assert data["items"][0]["name"] == sample_member.name
|
|
assert data["items"][0]["callsign"] == sample_member.callsign
|
|
|
|
def test_list_members_pagination(self, client_no_auth, sample_member):
|
|
"""Test member list pagination parameters."""
|
|
response = client_no_auth.get("/api/v1/members?limit=25&offset=10")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["limit"] == 25
|
|
assert data["offset"] == 10
|
|
|
|
def test_list_members_requires_read_auth(self, client_with_auth):
|
|
"""Test listing members requires read auth when configured."""
|
|
# Without auth header
|
|
response = client_with_auth.get("/api/v1/members")
|
|
assert response.status_code == 401
|
|
|
|
# With read key
|
|
response = client_with_auth.get(
|
|
"/api/v1/members",
|
|
headers={"Authorization": "Bearer test-read-key"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestGetMember:
|
|
"""Tests for GET /members/{member_id} endpoint."""
|
|
|
|
def test_get_member_success(self, client_no_auth, sample_member):
|
|
"""Test getting a specific member."""
|
|
response = client_no_auth.get(f"/api/v1/members/{sample_member.id}")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["member_id"] == sample_member.member_id
|
|
assert data["name"] == sample_member.name
|
|
assert data["callsign"] == sample_member.callsign
|
|
assert data["role"] == sample_member.role
|
|
assert data["description"] == sample_member.description
|
|
assert data["contact"] == sample_member.contact
|
|
|
|
def test_get_member_not_found(self, client_no_auth):
|
|
"""Test getting a non-existent member."""
|
|
response = client_no_auth.get("/api/v1/members/nonexistent-id")
|
|
assert response.status_code == 404
|
|
assert "not found" in response.json()["detail"].lower()
|
|
|
|
def test_get_member_requires_read_auth(self, client_with_auth, sample_member):
|
|
"""Test getting a member requires read auth when configured."""
|
|
# Without auth header
|
|
response = client_with_auth.get(f"/api/v1/members/{sample_member.id}")
|
|
assert response.status_code == 401
|
|
|
|
# With read key
|
|
response = client_with_auth.get(
|
|
f"/api/v1/members/{sample_member.id}",
|
|
headers={"Authorization": "Bearer test-read-key"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestCreateMember:
|
|
"""Tests for POST /members endpoint."""
|
|
|
|
def test_create_member_success(self, client_no_auth):
|
|
"""Test creating a new member."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/members",
|
|
json={
|
|
"member_id": "bob",
|
|
"name": "Bob Jones",
|
|
"callsign": "W2XYZ",
|
|
"role": "Member",
|
|
"description": "Regular member",
|
|
"contact": "bob@example.com",
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["member_id"] == "bob"
|
|
assert data["name"] == "Bob Jones"
|
|
assert data["callsign"] == "W2XYZ"
|
|
assert data["role"] == "Member"
|
|
assert "id" in data
|
|
assert "created_at" in data
|
|
|
|
def test_create_member_minimal(self, client_no_auth):
|
|
"""Test creating a member with only required fields."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/members",
|
|
json={
|
|
"member_id": "charlie",
|
|
"name": "Charlie Brown",
|
|
},
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert data["member_id"] == "charlie"
|
|
assert data["name"] == "Charlie Brown"
|
|
assert data["callsign"] is None
|
|
assert data["role"] is None
|
|
|
|
def test_create_member_duplicate_member_id(self, client_no_auth, sample_member):
|
|
"""Test creating a member with duplicate member_id fails."""
|
|
response = client_no_auth.post(
|
|
"/api/v1/members",
|
|
json={
|
|
"member_id": sample_member.member_id, # "alice" already exists
|
|
"name": "Another Alice",
|
|
},
|
|
)
|
|
assert response.status_code == 400
|
|
assert "already exists" in response.json()["detail"].lower()
|
|
|
|
def test_create_member_requires_admin_auth(self, client_with_auth):
|
|
"""Test creating a member requires admin auth."""
|
|
# Without auth
|
|
response = client_with_auth.post(
|
|
"/api/v1/members",
|
|
json={"member_id": "test", "name": "Test User"},
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
# With read key (not admin)
|
|
response = client_with_auth.post(
|
|
"/api/v1/members",
|
|
json={"member_id": "test", "name": "Test User"},
|
|
headers={"Authorization": "Bearer test-read-key"},
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
# With admin key
|
|
response = client_with_auth.post(
|
|
"/api/v1/members",
|
|
json={"member_id": "test", "name": "Test User"},
|
|
headers={"Authorization": "Bearer test-admin-key"},
|
|
)
|
|
assert response.status_code == 201
|
|
|
|
|
|
class TestUpdateMember:
|
|
"""Tests for PUT /members/{member_id} endpoint."""
|
|
|
|
def test_update_member_success(self, client_no_auth, sample_member):
|
|
"""Test updating a member."""
|
|
response = client_no_auth.put(
|
|
f"/api/v1/members/{sample_member.id}",
|
|
json={
|
|
"name": "Alice Johnson",
|
|
"role": "Super Admin",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["name"] == "Alice Johnson"
|
|
assert data["role"] == "Super Admin"
|
|
# Unchanged fields should remain
|
|
assert data["member_id"] == sample_member.member_id
|
|
assert data["callsign"] == sample_member.callsign
|
|
|
|
def test_update_member_change_member_id(self, client_no_auth, sample_member):
|
|
"""Test updating member_id."""
|
|
response = client_no_auth.put(
|
|
f"/api/v1/members/{sample_member.id}",
|
|
json={"member_id": "alice2"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["member_id"] == "alice2"
|
|
|
|
def test_update_member_member_id_collision(
|
|
self, client_no_auth, api_db_session, sample_member
|
|
):
|
|
"""Test updating member_id to one that already exists fails."""
|
|
from meshcore_hub.common.models import Member
|
|
|
|
# Create another member
|
|
other_member = Member(
|
|
member_id="bob",
|
|
name="Bob",
|
|
)
|
|
api_db_session.add(other_member)
|
|
api_db_session.commit()
|
|
|
|
# Try to change alice's member_id to "bob"
|
|
response = client_no_auth.put(
|
|
f"/api/v1/members/{sample_member.id}",
|
|
json={"member_id": "bob"},
|
|
)
|
|
assert response.status_code == 400
|
|
assert "already exists" in response.json()["detail"].lower()
|
|
|
|
def test_update_member_not_found(self, client_no_auth):
|
|
"""Test updating a non-existent member."""
|
|
response = client_no_auth.put(
|
|
"/api/v1/members/nonexistent-id",
|
|
json={"name": "New Name"},
|
|
)
|
|
assert response.status_code == 404
|
|
assert "not found" in response.json()["detail"].lower()
|
|
|
|
def test_update_member_requires_admin_auth(self, client_with_auth, sample_member):
|
|
"""Test updating a member requires admin auth."""
|
|
# Without auth
|
|
response = client_with_auth.put(
|
|
f"/api/v1/members/{sample_member.id}",
|
|
json={"name": "New Name"},
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
# With read key (not admin)
|
|
response = client_with_auth.put(
|
|
f"/api/v1/members/{sample_member.id}",
|
|
json={"name": "New Name"},
|
|
headers={"Authorization": "Bearer test-read-key"},
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
# With admin key
|
|
response = client_with_auth.put(
|
|
f"/api/v1/members/{sample_member.id}",
|
|
json={"name": "New Name"},
|
|
headers={"Authorization": "Bearer test-admin-key"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
class TestDeleteMember:
|
|
"""Tests for DELETE /members/{member_id} endpoint."""
|
|
|
|
def test_delete_member_success(self, client_no_auth, sample_member):
|
|
"""Test deleting a member."""
|
|
response = client_no_auth.delete(f"/api/v1/members/{sample_member.id}")
|
|
assert response.status_code == 204
|
|
|
|
# Verify it's deleted
|
|
response = client_no_auth.get(f"/api/v1/members/{sample_member.id}")
|
|
assert response.status_code == 404
|
|
|
|
def test_delete_member_not_found(self, client_no_auth):
|
|
"""Test deleting a non-existent member."""
|
|
response = client_no_auth.delete("/api/v1/members/nonexistent-id")
|
|
assert response.status_code == 404
|
|
assert "not found" in response.json()["detail"].lower()
|
|
|
|
def test_delete_member_requires_admin_auth(self, client_with_auth, sample_member):
|
|
"""Test deleting a member requires admin auth."""
|
|
# Without auth
|
|
response = client_with_auth.delete(f"/api/v1/members/{sample_member.id}")
|
|
assert response.status_code == 401
|
|
|
|
# With read key (not admin)
|
|
response = client_with_auth.delete(
|
|
f"/api/v1/members/{sample_member.id}",
|
|
headers={"Authorization": "Bearer test-read-key"},
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
# With admin key
|
|
response = client_with_auth.delete(
|
|
f"/api/v1/members/{sample_member.id}",
|
|
headers={"Authorization": "Bearer test-admin-key"},
|
|
)
|
|
assert response.status_code == 204
|