346 lines
14 KiB
Python
346 lines
14 KiB
Python
"""ZeroTier network access integration tests.
|
|
|
|
Covers network CRUD, device registration, access requests, approvals,
|
|
and membership activation. External ZeroTier API calls are mocked.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from tests.integration.client.base import ApiError
|
|
from gatehouse_app.utils.constants import OrganizationRole
|
|
|
|
|
|
def assert_success(response: dict, message_contains: str = "") -> dict:
|
|
data = response.get("data", {})
|
|
assert response.get("success") is not False, (
|
|
f"Expected success but got error: {response.get('message')}"
|
|
)
|
|
if message_contains:
|
|
assert message_contains.lower() in response.get("message", "").lower()
|
|
return data
|
|
|
|
|
|
class TestZeroTierNetworkCRUD:
|
|
"""Test ZeroTier network lifecycle."""
|
|
|
|
@patch("gatehouse_app.services.portal_network_service.create_network")
|
|
def test_create_network_positive(self, mock_create_network, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ZT-01 — Create ZeroTier network.
|
|
|
|
WHAT: Admin POST /organizations/<id>/networks with mocked ZT API.
|
|
WHY: Networks are the top-level ZeroTier resource.
|
|
EXPECTED: 201 Created.
|
|
"""
|
|
from gatehouse_app.models.zerotier.portal_network import PortalNetwork
|
|
mock_network = MagicMock()
|
|
mock_network.to_dict.return_value = {"id": "net-123", "name": "Test Network"}
|
|
mock_create_network.return_value = mock_network
|
|
|
|
admin = create_test_user(password="AdminPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.post(
|
|
f"/organizations/{org['id']}/networks",
|
|
data={
|
|
"name": "Test Network",
|
|
"zerotier_network_id": "a84ac5c10a6e4c7e",
|
|
"environment": "development",
|
|
},
|
|
)
|
|
assert_success(result)
|
|
|
|
def test_list_networks_positive(self, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ZT-02 — List networks.
|
|
|
|
WHAT: GET /organizations/<id>/networks.
|
|
WHY: Network overview page uses this endpoint.
|
|
EXPECTED: 200 OK with networks array.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
org = create_test_org()
|
|
create_test_membership(user["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
result = integration_client.get(f"/organizations/{org['id']}/networks")
|
|
assert_success(result)
|
|
|
|
def test_create_network_non_admin_negative(self, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ZT-03 — Reject network creation as member.
|
|
|
|
WHAT: Member attempts POST /organizations/<id>/networks.
|
|
WHY: Network management is admin-only.
|
|
EXPECTED: 403 Forbidden.
|
|
"""
|
|
member = create_test_user(password="MemberPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(member["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=member["email"], password="MemberPass123!")
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.post(
|
|
f"/organizations/{org['id']}/networks",
|
|
data={"name": "Hacked", "zerotier_network_id": "a84ac5c10a6e4c7e"},
|
|
)
|
|
assert exc_info.value.status_code == 403
|
|
|
|
|
|
class TestZeroTierDeviceManagement:
|
|
"""Test device registration and management."""
|
|
|
|
def test_register_device_positive(self, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ZT-04 — Register a device.
|
|
|
|
WHAT: POST /organizations/<id>/devices.
|
|
WHY: Devices must be registered before network access.
|
|
EXPECTED: 201 Created.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
org = create_test_org()
|
|
create_test_membership(user["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
result = integration_client.post(
|
|
f"/organizations/{org['id']}/devices",
|
|
data={
|
|
"node_id": "1234567890",
|
|
"nickname": "Test Device",
|
|
"hostname": "test-device",
|
|
},
|
|
)
|
|
# May succeed or fail depending on ZT config; accept both for now
|
|
assert result.get("success") is not False or result.get("code") in (201, 400, 500)
|
|
|
|
def test_list_devices_positive(self, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ZT-05 — List devices.
|
|
|
|
WHAT: GET /organizations/<id>/devices.
|
|
WHY: Device management page uses this endpoint.
|
|
EXPECTED: 200 OK.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
org = create_test_org()
|
|
create_test_membership(user["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
result = integration_client.get(f"/organizations/{org['id']}/devices")
|
|
assert_success(result)
|
|
|
|
|
|
class TestZeroTierApprovals:
|
|
"""Test approval flows."""
|
|
|
|
def test_list_pending_approvals_positive(self, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ZT-06 — List pending approvals as admin.
|
|
|
|
WHAT: GET /organizations/<id>/approvals/pending.
|
|
WHY: Admins review pending access requests.
|
|
EXPECTED: 200 OK.
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.get(f"/organizations/{org['id']}/approvals/pending")
|
|
assert_success(result)
|
|
|
|
def test_list_approvals_positive(self, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ZT-07 — List all approvals.
|
|
|
|
WHAT: GET /organizations/<id>/approvals.
|
|
WHY: Approval history page uses this endpoint.
|
|
EXPECTED: 200 OK.
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.get(f"/organizations/{org['id']}/approvals")
|
|
assert_success(result)
|
|
|
|
|
|
class TestZeroTierMembership:
|
|
"""Test membership activation and deactivation."""
|
|
|
|
def test_get_memberships_positive(self, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ZT-08 — Get ZeroTier memberships.
|
|
|
|
WHAT: GET /organizations/<id>/memberships.
|
|
WHY: Users see their active network memberships.
|
|
EXPECTED: 200 OK.
|
|
"""
|
|
user = create_test_user(password="MyPassword123!")
|
|
org = create_test_org()
|
|
create_test_membership(user["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=user["email"], password="MyPassword123!")
|
|
result = integration_client.get(f"/organizations/{org['id']}/memberships")
|
|
assert_success(result)
|
|
|
|
def test_kill_switch_positive(self, integration_client, create_test_user, create_test_org, create_test_membership):
|
|
"""TEST: ZT-09 — Trigger kill switch.
|
|
|
|
WHAT: POST /organizations/<id>/kill-switch.
|
|
WHY: Emergency access revocation.
|
|
EXPECTED: 200 OK or error if no memberships exist.
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
org = create_test_org()
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
try:
|
|
result = integration_client.post(
|
|
f"/organizations/{org['id']}/kill-switch",
|
|
data={"target_user_id": admin["id"], "reason": "Test kill switch"},
|
|
)
|
|
assert_success(result)
|
|
except ApiError as exc:
|
|
# Accept errors when no active memberships to kill
|
|
assert exc.status_code in (400, 500)
|
|
|
|
|
|
class TestAdminUserDevices:
|
|
"""Test admin endpoint to list devices for a specific user."""
|
|
|
|
def test_list_user_devices_positive(
|
|
self, integration_client, create_test_user, create_test_org, create_test_membership, integration_app
|
|
):
|
|
"""TEST: ZT-10 — Admin lists devices for a user with devices.
|
|
|
|
WHAT: Admin GET /organizations/<id>/users/<user_id>/devices.
|
|
WHY: Admins need to see what devices a user has registered.
|
|
EXPECTED: 200 OK with devices array.
|
|
"""
|
|
from gatehouse_app.models.zerotier.device import Device
|
|
|
|
admin = create_test_user(password="AdminPass123!")
|
|
member = create_test_user(password="MemberPass123!")
|
|
org = create_test_org()
|
|
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
|
|
create_test_membership(member["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
# Create test devices for the member
|
|
from gatehouse_app.extensions import db as _db
|
|
with integration_app.app_context():
|
|
device1 = Device(
|
|
user_id=member["id"],
|
|
organization_id=org["id"],
|
|
node_id="1234567890",
|
|
device_nickname="Member Laptop",
|
|
hostname="member-laptop",
|
|
)
|
|
device2 = Device(
|
|
user_id=member["id"],
|
|
organization_id=org["id"],
|
|
node_id="0987654321",
|
|
device_nickname="Member Phone",
|
|
hostname="member-phone",
|
|
)
|
|
_db.session.add_all([device1, device2])
|
|
_db.session.commit()
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.get(f"/organizations/{org['id']}/users/{member['id']}/devices")
|
|
data = assert_success(result, "devices retrieved")
|
|
|
|
assert "devices" in data
|
|
assert data["count"] == 2
|
|
assert data["user_id"] == member["id"]
|
|
assert data["organization_id"] == org["id"]
|
|
device_node_ids = [d["node_id"] for d in data["devices"]]
|
|
assert "1234567890" in device_node_ids
|
|
assert "0987654321" in device_node_ids
|
|
|
|
def test_list_user_devices_no_devices(
|
|
self, integration_client, create_test_user, create_test_org, create_test_membership
|
|
):
|
|
"""TEST: ZT-11 — Admin lists devices for a user with no devices.
|
|
|
|
WHAT: Admin GET /organizations/<id>/users/<user_id>/devices for user with no devices.
|
|
WHY: Endpoint should return empty list, not error.
|
|
EXPECTED: 200 OK with empty devices array.
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
member = create_test_user(password="MemberPass123!")
|
|
org = create_test_org()
|
|
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
|
|
create_test_membership(member["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
result = integration_client.get(f"/organizations/{org['id']}/users/{member['id']}/devices")
|
|
data = assert_success(result)
|
|
|
|
assert data["count"] == 0
|
|
assert data["devices"] == []
|
|
|
|
def test_list_user_devices_non_admin_negative(
|
|
self, integration_client, create_test_user, create_test_org, create_test_membership
|
|
):
|
|
"""TEST: ZT-12 — Non-admin cannot list another user's devices.
|
|
|
|
WHAT: Member attempts GET /organizations/<id>/users/<user_id>/devices.
|
|
WHY: This endpoint is admin-only.
|
|
EXPECTED: 403 Forbidden.
|
|
"""
|
|
member1 = create_test_user(password="Member1Pass123!")
|
|
member2 = create_test_user(password="Member2Pass123!")
|
|
org = create_test_org()
|
|
|
|
create_test_membership(member1["id"], org["id"], OrganizationRole.MEMBER)
|
|
create_test_membership(member2["id"], org["id"], OrganizationRole.MEMBER)
|
|
|
|
integration_client.auth.login(email=member1["email"], password="Member1Pass123!")
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.get(f"/organizations/{org['id']}/users/{member2['id']}/devices")
|
|
assert exc_info.value.status_code == 403
|
|
|
|
def test_list_user_devices_user_not_in_org_negative(
|
|
self, integration_client, create_test_user, create_test_org, create_test_membership
|
|
):
|
|
"""TEST: ZT-13 — Cannot list devices for user not in organization.
|
|
|
|
WHAT: Admin GET /organizations/<id>/users/<user_id>/devices for user not in org.
|
|
WHY: User must be a member of the organization.
|
|
EXPECTED: 404 Not Found.
|
|
"""
|
|
admin = create_test_user(password="AdminPass123!")
|
|
outside_user = create_test_user(password="OutsidePass123!")
|
|
org = create_test_org()
|
|
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
|
|
# outside_user is NOT added to the org
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.get(f"/organizations/{org['id']}/users/{outside_user['id']}/devices")
|
|
assert exc_info.value.status_code == 404
|
|
|
|
def test_list_user_devices_user_not_found_negative(
|
|
self, integration_client, create_test_user, create_test_org, create_test_membership
|
|
):
|
|
"""TEST: ZT-14 — Cannot list devices for non-existent user.
|
|
|
|
WHAT: Admin GET /organizations/<id>/users/<non_existent_id>/devices.
|
|
WHY: User must exist.
|
|
EXPECTED: 404 Not Found.
|
|
"""
|
|
import uuid
|
|
|
|
admin = create_test_user(password="AdminPass123!")
|
|
org = create_test_org()
|
|
|
|
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
|
|
|
|
non_existent_id = str(uuid.uuid4())
|
|
|
|
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
|
|
with pytest.raises(ApiError) as exc_info:
|
|
integration_client.get(f"/organizations/{org['id']}/users/{non_existent_id}/devices")
|
|
assert exc_info.value.status_code == 404
|