import pytest from rest_framework.test import APIClient from apps.notifications.services import store as services from apps.notifications.services import RedisNotificationStore from apps.notifications.tests.test_services import FakeRedis from apps.projects.models import Project, ProjectMembership from apps.users.models import User from apps.workspaces.models import Workspace, WorkspaceMembership @pytest.fixture() def fake_redis(monkeypatch): redis = FakeRedis() monkeypatch.setattr(services, "redis_client", redis) return redis @pytest.fixture() def api_client(): return APIClient() def _create_user(index: int) -> User: return User.objects.create_user( mobile=f"091200000{index:02d}", password="secret123", first_name=f"User{index}", ) def _notifications_for(user): notifications, _ = RedisNotificationStore.list( str(user.id), paginate=False, ) return notifications @pytest.fixture() def owner(db): return _create_user(1) @pytest.fixture() def member(db): return _create_user(2) @pytest.fixture() def another_member(db): return _create_user(3) @pytest.fixture() def third_member(db): return _create_user(4) @pytest.fixture() def fourth_member(db): return _create_user(5) def test_workspace_create_notifies_initial_members_not_owner( fake_redis, api_client, owner, member ): api_client.force_authenticate(user=owner) response = api_client.post( "/api/workspaces/", { "name": "Ops", "description": "Workspace", "members": [ {"user_id": str(member.id), "role": WorkspaceMembership.Role.ADMIN} ], }, format="json", ) assert response.status_code == 201 owner_notifications = _notifications_for(owner) member_notifications = _notifications_for(member) assert owner_notifications == [] assert len(member_notifications) == 1 assert member_notifications[0]["type"] == "workspace_membership_added" assert member_notifications[0]["meta"]["workspace_name"] == "Ops" assert member_notifications[0]["meta"]["new_role"] == WorkspaceMembership.Role.ADMIN def test_workspace_membership_crud_emits_add_role_change_deactivate_and_remove( fake_redis, api_client, owner, member ): workspace = Workspace.objects.create(name="Design", description="", owner=owner) api_client.force_authenticate(user=owner) create_response = api_client.post( "/api/workspace-memberships/", { "workspace": str(workspace.id), "user": str(member.id), "role": WorkspaceMembership.Role.MEMBER, "is_active": True, }, format="json", ) assert create_response.status_code == 201 membership_id = create_response.data["id"] notifications = _notifications_for(member) assert [item["type"] for item in notifications] == ["workspace_membership_added"] role_response = api_client.patch( f"/api/workspace-memberships/{membership_id}/", {"role": WorkspaceMembership.Role.ADMIN}, format="json", ) assert role_response.status_code == 200 deactivate_response = api_client.patch( f"/api/workspace-memberships/{membership_id}/", {"is_active": False}, format="json", ) assert deactivate_response.status_code == 200 remove_response = api_client.delete(f"/api/workspace-memberships/{membership_id}/") assert remove_response.status_code == 204 notifications = _notifications_for(member) assert [item["type"] for item in notifications] == [ "workspace_membership_removed", "workspace_membership_deactivated", "workspace_membership_role_changed", "workspace_membership_added", ] def test_workspace_membership_update_skips_self_notifications( fake_redis, api_client, owner ): workspace = Workspace.objects.create(name="Product", description="", owner=owner) owner_membership = WorkspaceMembership.objects.get( workspace=workspace, user=owner, is_deleted=False, ) api_client.force_authenticate(user=owner) response = api_client.patch( f"/api/workspace-memberships/{owner_membership.id}/", {"role": WorkspaceMembership.Role.OWNER}, format="json", ) assert response.status_code == 200 assert _notifications_for(owner) == [] def test_project_create_notifies_initial_members_not_creator( fake_redis, api_client, owner, member ): workspace = Workspace.objects.create(name="Engineering", description="", owner=owner) api_client.force_authenticate(user=owner) response = api_client.post( "/api/projects/", { "workspace": str(workspace.id), "name": "API", "description": "", "client": None, "members": [ {"user_id": str(member.id), "role": ProjectMembership.Role.MEMBER} ], }, format="json", ) assert response.status_code == 201 assert _notifications_for(owner) == [] notifications = _notifications_for(member) assert len(notifications) == 1 assert notifications[0]["type"] == "project_membership_added" assert notifications[0]["meta"]["project_name"] == "API" def test_project_update_full_sync_notifies_only_real_deltas( fake_redis, api_client, owner, member, another_member, third_member, fourth_member ): workspace = Workspace.objects.create(name="Build", description="", owner=owner) project = Project.objects.create(workspace=workspace, name="Platform", description="") ProjectMembership.objects.create( project=project, user=owner, role=ProjectMembership.Role.MANAGER, is_active=True, ) ProjectMembership.objects.create( project=project, user=member, role=ProjectMembership.Role.MEMBER, is_active=True, ) ProjectMembership.objects.create( project=project, user=another_member, role=ProjectMembership.Role.MEMBER, is_active=True, ) ProjectMembership.objects.create( project=project, user=third_member, role=ProjectMembership.Role.MEMBER, is_active=True, ) api_client.force_authenticate(user=owner) response = api_client.patch( f"/api/projects/{project.id}/", { "client": None, "members": [ {"user_id": str(member.id), "role": ProjectMembership.Role.MANAGER}, {"user_id": str(third_member.id), "role": ProjectMembership.Role.MEMBER}, {"user_id": str(fourth_member.id), "role": ProjectMembership.Role.MEMBER}, ], }, format="json", ) assert response.status_code == 200 assert [item["type"] for item in _notifications_for(member)] == [ "project_membership_role_changed" ] assert [item["type"] for item in _notifications_for(another_member)] == [ "project_membership_deactivated" ] assert _notifications_for(third_member) == [] assert [item["type"] for item in _notifications_for(fourth_member)] == [ "project_membership_added" ] assert _notifications_for(owner) == [] def test_project_membership_crud_emits_add_role_change_deactivate_and_remove( fake_redis, api_client, owner, member ): workspace = Workspace.objects.create(name="Data", description="", owner=owner) project = Project.objects.create(workspace=workspace, name="Warehouse", description="") ProjectMembership.objects.create( project=project, user=owner, role=ProjectMembership.Role.MANAGER, is_active=True, ) api_client.force_authenticate(user=owner) create_response = api_client.post( "/api/memberships/", { "project_id": str(project.id), "user_id": str(member.id), "role": ProjectMembership.Role.MEMBER, }, format="json", ) assert create_response.status_code == 201 membership_id = create_response.data["id"] role_response = api_client.patch( f"/api/memberships/{membership_id}/", {"role": ProjectMembership.Role.MANAGER}, format="json", ) assert role_response.status_code == 200 deactivate_response = api_client.patch( f"/api/memberships/{membership_id}/", {"is_active": False}, format="json", ) assert deactivate_response.status_code == 200 remove_response = api_client.delete(f"/api/memberships/{membership_id}/") assert remove_response.status_code == 204 notifications = _notifications_for(member) assert [item["type"] for item in notifications] == [ "project_membership_removed", "project_membership_deactivated", "project_membership_role_changed", "project_membership_added", ]