"""Tenant isolation suite (AGENTS.backend-packages.md ยง11). Proves tenant A cannot read, write, update, or delete tenant B's rows and that cross-tenant access returns 404 through the API. """ import pytest from infrasynth.notifications.models import NotificationDispatch from infrasynth.scheduler.models import ScheduledTask from infrasynth.security.models import Grant from infrasynth.tenancy.context import tenant_context from infrasynth.tenancy.models import Tenant from infrasynth.webhooks.models import OutboundEndpoint from infrasynth.workflows.models import Workflow pytestmark = pytest.mark.django_db @pytest.fixture def other_tenant(): return Tenant.objects.create(slug="other-ws", name="Other Workspace") def _seed(tenant): ScheduledTask.all_objects.create( tenant=tenant, name="task", task_path="tests.helpers.noop_task", schedule_type="manual" ) OutboundEndpoint.all_objects.create(tenant=tenant, name="ep", url="https://x.test/h", secret="s") Workflow.all_objects.create(tenant=tenant, slug="wf", name="WF") NotificationDispatch.all_objects.create(tenant=tenant, recipient="a@b.c", channel="email", subject="s", body="b") class TestManagerIsolation: def test_tenant_a_cannot_read_tenant_b(self, tenant, other_tenant): _seed(tenant) with tenant_context(other_tenant): assert ScheduledTask.objects.count() == 0 assert OutboundEndpoint.objects.count() == 0 assert Workflow.objects.count() == 0 assert NotificationDispatch.objects.count() == 0 def test_tenant_b_cannot_read_tenant_a(self, tenant, other_tenant): _seed(other_tenant) with tenant_context(tenant): assert ScheduledTask.objects.count() == 0 assert OutboundEndpoint.objects.count() == 0 def test_cross_tenant_update_is_invisible(self, tenant, other_tenant): _seed(other_tenant) with tenant_context(tenant), pytest.raises(ScheduledTask.DoesNotExist): ScheduledTask.objects.get(name="task") def test_cross_tenant_delete_is_invisible(self, tenant, other_tenant): _seed(other_tenant) with tenant_context(tenant): assert ScheduledTask.objects.filter(name="task").delete()[0] == 0 with tenant_context(other_tenant): assert ScheduledTask.objects.filter(name="task").exists() def test_grant_scoped(self, tenant, other_tenant, user): Grant.all_objects.create(tenant=tenant, user=user, codename="a") Grant.all_objects.create(tenant=other_tenant, user=user, codename="b") with tenant_context(tenant): assert list(Grant.objects.values_list("codename", flat=True)) == ["a"] class TestCrossTenantApiReturns404: def test_other_tenant_task_is_404(self, authenticated_client, tenant, other_tenant): task = ScheduledTask.all_objects.create( tenant=other_tenant, name="secret", task_path="tests.helpers.noop_task", schedule_type="manual" ) # authenticated_client's context tenant is `tenant`, so B's row is invisible. assert authenticated_client.get(f"/api/v1/scheduler/tasks/{task.pk}/").status_code == 404 assert authenticated_client.delete(f"/api/v1/scheduler/tasks/{task.pk}/").status_code == 404 def test_other_tenant_endpoint_is_404(self, authenticated_client, other_tenant): endpoint = OutboundEndpoint.all_objects.create( tenant=other_tenant, name="secret", url="https://x.test/h", secret="s" ) assert authenticated_client.get(f"/api/v1/webhooks/outbound/endpoints/{endpoint.pk}/").status_code == 404