from __future__ import annotations import unittest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from govoplan_access.backend.db.models import Account, Group, User from govoplan_campaign.backend.capabilities import CampaignAccessService from govoplan_campaign.backend.db.models import Campaign, CampaignShare from govoplan_core.core.access import PrincipalRef from govoplan_core.db.base import Base TENANT_ID = "tenant-1" USER_ID = "user-1" OTHER_USER_ID = "user-2" GROUP_ID = "group-1" class CampaignAccessProviderTests(unittest.TestCase): def test_campaign_access_provider_explains_owner_share_admin_and_missing_resources(self) -> None: session = _session() _seed_access_subjects(session) owned = Campaign(id="campaign-owned", tenant_id=TENANT_ID, owner_user_id=USER_ID, external_id="owned", name="Owned campaign") shared = Campaign(id="campaign-shared", tenant_id=TENANT_ID, owner_user_id=OTHER_USER_ID, external_id="shared", name="Shared campaign") session.add_all([ owned, shared, CampaignShare(id="share-group", tenant_id=TENANT_ID, campaign_id=shared.id, target_type="group", target_id=GROUP_ID, permission="read"), ]) session.commit() service = CampaignAccessService() owned_items = service.explain_resource_provenance(session, _principal(), resource_type="campaign", resource_id=owned.id, action="campaigns:campaign:read") shared_items = service.explain_resource_provenance(session, _principal(group_ids={GROUP_ID}), resource_type="campaign", resource_id=shared.id, action="campaigns:campaign:read") admin_items = service.explain_resource_provenance(session, _principal(scopes={"tenant:*"}), resource_type="campaign", resource_id=shared.id, action="campaigns:campaign:read") missing_items = service.explain_resource_provenance(session, _principal(), resource_type="campaign", resource_id="missing-campaign", action="campaigns:campaign:read") self.assertTrue(any(item.kind == "resource" and item.source == "campaigns.campaign" and item.id == owned.id for item in owned_items)) self.assertTrue(any(item.kind == "owner" and item.id == USER_ID for item in owned_items)) self.assertTrue(any(item.kind == "share" and item.id == "share-group" for item in shared_items)) self.assertTrue(any(item.kind == "policy" and item.id == "tenant:*" for item in admin_items)) self.assertEqual("campaigns.not_found", missing_items[0].source) self.assertIs(missing_items[0].details["found"], False) def test_campaign_access_provider_requires_write_share_for_write_actions(self) -> None: session = _session() _seed_access_subjects(session) campaign = Campaign(id="campaign-write", tenant_id=TENANT_ID, owner_user_id=OTHER_USER_ID, external_id="write", name="Write campaign") session.add_all([ campaign, CampaignShare(id="share-read", tenant_id=TENANT_ID, campaign_id=campaign.id, target_type="group", target_id=GROUP_ID, permission="read"), CampaignShare(id="share-write", tenant_id=TENANT_ID, campaign_id=campaign.id, target_type="user", target_id=USER_ID, permission="write"), ]) session.commit() service = CampaignAccessService() items = service.explain_resource_provenance( session, _principal(group_ids={GROUP_ID}), resource_type="campaign", resource_id=campaign.id, action="campaigns:campaign:update", ) self.assertTrue(any(item.kind == "share" and item.id == "share-write" for item in items)) self.assertFalse(any(item.kind == "share" and item.id == "share-read" for item in items)) def _session(): engine = create_engine("sqlite:///:memory:", future=True) Base.metadata.create_all(bind=engine, tables=[Account.__table__, User.__table__, Group.__table__, Campaign.__table__, CampaignShare.__table__]) return sessionmaker(bind=engine, future=True)() def _seed_access_subjects(session) -> None: session.add_all([ Account(id="account-1", email="one@example.test", normalized_email="one@example.test"), Account(id="account-2", email="two@example.test", normalized_email="two@example.test"), User(id=USER_ID, tenant_id=TENANT_ID, account_id="account-1", email="one@example.test"), User(id=OTHER_USER_ID, tenant_id=TENANT_ID, account_id="account-2", email="two@example.test"), Group(id=GROUP_ID, tenant_id=TENANT_ID, slug="group", name="Group"), ]) session.commit() def _principal(*, scopes: set[str] | None = None, group_ids: set[str] | None = None) -> PrincipalRef: return PrincipalRef( account_id="account-1", membership_id=USER_ID, tenant_id=TENANT_ID, scopes=frozenset(scopes or {"campaigns:campaign:read"}), group_ids=frozenset(group_ids or set()), )