Updated ZeroTier network membership flow and logic

This commit is contained in:
2026-05-28 05:42:04 +00:00
parent 2342a1aab6
commit 2c8160d78e
5 changed files with 524 additions and 6 deletions
+286
View File
@@ -203,6 +203,80 @@ class TestZeroTierMembership:
assert exc.status_code in (400, 500)
class TestZeroTierJoinNetwork:
"""Test joining a network with a registered device."""
@patch("gatehouse_app.services.network_access_service._ensure_zerotier_member")
def test_rejoin_after_deactivation_positive(
self,
mock_ensure_member,
integration_client,
create_test_user,
create_test_org,
create_test_membership,
integration_app,
):
"""TEST: ZT-15 — Re-join network after deactivation.
WHAT: User with deactivated membership (APPROVED + active=False)
POSTs to join-network for the same device and network.
WHY: Deactivated memberships should not block re-join attempts.
EXPECTED: 201 Created (re-opens existing request), not 409 Conflict.
"""
from gatehouse_app.models.zerotier.device import Device
from gatehouse_app.models.zerotier.portal_network import PortalNetwork
from gatehouse_app.models.zerotier.network_access_request import NetworkAccessRequest
from gatehouse_app.extensions import db as _db
from gatehouse_app.utils.constants import ApprovalState, NetworkRequestMode
user = create_test_user(password="MyPassword123!")
org = create_test_org()
create_test_membership(user["id"], org["id"], OrganizationRole.MEMBER)
with integration_app.app_context():
device = Device(
user_id=user["id"],
organization_id=org["id"],
node_id="1234567890",
device_nickname="Test Device",
hostname="test-device",
)
_db.session.add(device)
_db.session.flush()
device_id = device.id
portal_network = PortalNetwork(
organization_id=org["id"],
name="Test Network",
owner_user_id=user["id"],
zerotier_network_id="a84ac5c10a6e4c7e",
request_mode=NetworkRequestMode.OPEN,
)
_db.session.add(portal_network)
_db.session.flush()
portal_network_id = portal_network.id
deactivated_request = NetworkAccessRequest(
organization_id=org["id"],
user_id=user["id"],
device_id=device_id,
portal_network_id=portal_network_id,
status=ApprovalState.APPROVED,
active=False,
)
_db.session.add(deactivated_request)
_db.session.commit()
integration_client.auth.login(email=user["email"], password="MyPassword123!")
result = integration_client.post(
f"/organizations/{org['id']}/devices/{device_id}/join-network/{portal_network_id}",
)
data = assert_success(result, "joined network successfully")
assert result.get("code") == 201
assert "membership" in data
mock_ensure_member.assert_called_once()
class TestAdminUserDevices:
"""Test admin endpoint to list devices for a specific user."""
@@ -343,3 +417,215 @@ class TestAdminUserDevices:
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
class TestAdminForceDeleteMembership:
"""Test admin force-delete of network access requests."""
@patch("gatehouse_app.services.zerotier_api_service.deauthorize_member")
@patch("gatehouse_app.services.zerotier_api_service.delete_network_member")
def test_force_delete_active_membership_positive(
self,
mock_delete_member,
mock_deauthorize_member,
integration_client,
create_test_user,
create_test_org,
create_test_membership,
integration_app,
):
"""TEST: ZT-16 — Admin force-deletes an active membership.
WHAT: Admin calls DELETE admin/memberships/<id> on an active,
non-soft-deleted request. Should deactivate, remove from ZT,
and hard-delete the DB record in one step.
EXPECTED: 200 OK, request no longer exists in DB.
"""
from gatehouse_app.models.zerotier.device import Device
from gatehouse_app.models.zerotier.portal_network import PortalNetwork
from gatehouse_app.models.zerotier.network_access_request import NetworkAccessRequest
from gatehouse_app.extensions import db as _db
from gatehouse_app.utils.constants import ApprovalState, NetworkRequestMode
admin = create_test_user(password="AdminPass123!")
user = create_test_user(password="UserPass123!")
org = create_test_org()
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
create_test_membership(user["id"], org["id"], OrganizationRole.MEMBER)
with integration_app.app_context():
device = Device(
user_id=user["id"],
organization_id=org["id"],
node_id="1234567890",
device_nickname="Test Device",
hostname="test-device",
)
_db.session.add(device)
_db.session.flush()
device_id = device.id
portal_network = PortalNetwork(
organization_id=org["id"],
name="Test Network",
owner_user_id=admin["id"],
zerotier_network_id="a84ac5c10a6e4c7e",
request_mode=NetworkRequestMode.OPEN,
)
_db.session.add(portal_network)
_db.session.flush()
portal_network_id = portal_network.id
request = NetworkAccessRequest(
organization_id=org["id"],
user_id=user["id"],
device_id=device_id,
portal_network_id=portal_network_id,
status=ApprovalState.APPROVED,
active=True,
)
_db.session.add(request)
_db.session.commit()
request_id = request.id
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
result = integration_client.delete(
f"/organizations/{org['id']}/admin/memberships/{request_id}",
)
assert_success(result, "permanently deleted")
# Verify request is gone from DB
with integration_app.app_context():
deleted = NetworkAccessRequest.query.get(request_id)
assert deleted is None
mock_deauthorize_member.assert_called_once()
mock_delete_member.assert_called_once()
@patch("gatehouse_app.services.zerotier_api_service.delete_network_member")
def test_force_delete_soft_deleted_membership_positive(
self,
mock_delete_member,
integration_client,
create_test_user,
create_test_org,
create_test_membership,
integration_app,
):
"""TEST: ZT-17 — Admin force-deletes an already-soft-deleted membership.
WHAT: Admin calls DELETE admin/memberships/<id> on a soft-deleted
request. Should still work (remove from ZT, hard-delete).
EXPECTED: 200 OK.
"""
from datetime import datetime, timezone
from gatehouse_app.models.zerotier.device import Device
from gatehouse_app.models.zerotier.portal_network import PortalNetwork
from gatehouse_app.models.zerotier.network_access_request import NetworkAccessRequest
from gatehouse_app.extensions import db as _db
from gatehouse_app.utils.constants import ApprovalState, NetworkRequestMode
admin = create_test_user(password="AdminPass123!")
user = create_test_user(password="UserPass123!")
org = create_test_org()
create_test_membership(admin["id"], org["id"], OrganizationRole.ADMIN)
create_test_membership(user["id"], org["id"], OrganizationRole.MEMBER)
with integration_app.app_context():
device = Device(
user_id=user["id"],
organization_id=org["id"],
node_id="1234567890",
device_nickname="Test Device",
hostname="test-device",
)
_db.session.add(device)
_db.session.flush()
device_id = device.id
portal_network = PortalNetwork(
organization_id=org["id"],
name="Test Network",
owner_user_id=admin["id"],
zerotier_network_id="a84ac5c10a6e4c7e",
request_mode=NetworkRequestMode.OPEN,
)
_db.session.add(portal_network)
_db.session.flush()
portal_network_id = portal_network.id
request = NetworkAccessRequest(
organization_id=org["id"],
user_id=user["id"],
device_id=device_id,
portal_network_id=portal_network_id,
status=ApprovalState.APPROVED,
active=False,
deleted_at=datetime.now(timezone.utc),
)
_db.session.add(request)
_db.session.commit()
request_id = request.id
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
result = integration_client.delete(
f"/organizations/{org['id']}/admin/memberships/{request_id}",
)
assert_success(result, "permanently deleted")
# Verify request is gone from DB
with integration_app.app_context():
deleted = NetworkAccessRequest.query.get(request_id)
assert deleted is None
mock_delete_member.assert_called_once()
def test_force_delete_non_existent_membership_negative(
self,
integration_client,
create_test_user,
create_test_org,
create_test_membership,
):
"""TEST: ZT-18 — Admin force-deletes a non-existent membership.
WHAT: Admin calls DELETE admin/memberships/<non_existent_id>.
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)
integration_client.auth.login(email=admin["email"], password="AdminPass123!")
with pytest.raises(ApiError) as exc_info:
integration_client.delete(
f"/organizations/{org['id']}/admin/memberships/{uuid.uuid4()}",
)
assert exc_info.value.status_code == 404
def test_force_delete_non_admin_negative(
self,
integration_client,
create_test_user,
create_test_org,
create_test_membership,
):
"""TEST: ZT-19 — Non-admin cannot force-delete a membership.
WHAT: A MEMBER calls DELETE admin/memberships/<id>.
EXPECTED: 403 Forbidden.
"""
import uuid
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.delete(
f"/organizations/{org['id']}/admin/memberships/{uuid.uuid4()}",
)
assert exc_info.value.status_code == 403