Updated ZeroTier network membership flow and logic
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user