from __future__ import annotations import httpx import pytest @pytest.mark.integration def test_group_creation_and_member_invitation(api_client: httpx.Client, create_test_user): """ Scenario: Group creation and member invitation by email. """ owner = create_test_user(email_prefix="grp_lead") invitee = create_test_user(email_prefix="grp_join") # Create group res_grp = api_client.post("/api/v1/groups", json={"name": "Home Maintenance Crew"}, headers=owner["headers"]) assert res_grp.status_code == 201 group_id = res_grp.json()["id"] assert res_grp.json()["ownerEmail"] == owner["email"] # Invite member res_invite = api_client.post( f"/api/v1/groups/{group_id}/members", json={"email": invitee["email"]}, headers=owner["headers"], ) assert res_invite.status_code == 200 assert invitee["email"] in res_invite.json()["memberEmails"] @pytest.mark.integration def test_cross_user_shared_item_collaboration(api_client: httpx.Client, create_test_user): """ Scenario: Shared group items creation and cross-member editing. All group members can create, view, and edit shared items. """ user_a = create_test_user(email_prefix="collab_a") user_b = create_test_user(email_prefix="collab_b") # User A creates group and invites User B grp_res = api_client.post("/api/v1/groups", json={"name": "Shared Fleet"}, headers=user_a["headers"]) group_id = grp_res.json()["id"] api_client.post(f"/api/v1/groups/{group_id}/members", json={"email": user_b["email"]}, headers=user_a["headers"]) # User B creates shared item item_res = api_client.post( "/api/v1/items", json={ "name": "Shared Van Oil Change", "lifecycleType": "maintenance", "dueDate": "2026-11-20", "groupId": group_id, }, headers=user_b["headers"], ) assert item_res.status_code == 201 item_id = item_res.json()["id"] # User A lists items for the group and sees User B's item list_res = api_client.get(f"/api/v1/items?groupId={group_id}", headers=user_a["headers"]) assert list_res.status_code == 200 group_items = list_res.json() assert any(it["id"] == item_id for it in group_items) # User A updates the item created by User B patch_res = api_client.patch( f"/api/v1/items/{item_id}", json={"name": "Shared Van Full 50k Service"}, headers=user_a["headers"], ) assert patch_res.status_code == 200 assert patch_res.json()["name"] == "Shared Van Full 50k Service" # User B fetches the item and sees the updated name get_res = api_client.get(f"/api/v1/items/{item_id}", headers=user_b["headers"]) assert get_res.status_code == 200 assert get_res.json()["name"] == "Shared Van Full 50k Service" @pytest.mark.integration def test_unauthorized_non_member_access_denied(api_client: httpx.Client, create_test_user): """ Scenario: Multi-tenant security boundaries. Non-member users cannot see group in their group list, cannot access group items, and cannot create items in groups they do not belong to. """ owner = create_test_user(email_prefix="sec_owner") outsider = create_test_user(email_prefix="sec_outsider") # Create group grp_res = api_client.post("/api/v1/groups", json={"name": "Private Group"}, headers=owner["headers"]) group_id = grp_res.json()["id"] # Outsider listing groups does not see the private group outsider_groups = api_client.get("/api/v1/groups", headers=outsider["headers"]).json() assert not any(g["id"] == group_id for g in outsider_groups) # Outsider querying group items receives empty list outsider_items = api_client.get(f"/api/v1/items?groupId={group_id}", headers=outsider["headers"]).json() assert outsider_items == [] # Outsider tries to create item in group -> rejected with 400 post_res = api_client.post( "/api/v1/items", json={"name": "Illegal Item", "lifecycleType": "renewal", "groupId": group_id}, headers=outsider["headers"], ) assert post_res.status_code == 400 assert "not authorized" in post_res.json().get("detail", "").lower() or "error" in post_res.json() # Outsider cannot invite members to the group invite_res = api_client.post( f"/api/v1/groups/{group_id}/members", json={"email": "hacker@example.com"}, headers=outsider["headers"], ) assert invite_res.status_code == 400 @pytest.mark.integration def test_member_removal_and_non_owner_deletion_restriction(api_client: httpx.Client, create_test_user): """ Scenario: Member removal and non-owner deletion rejection. """ owner = create_test_user(email_prefix="perm_owner") member = create_test_user(email_prefix="perm_member") # Create group and add member grp_res = api_client.post("/api/v1/groups", json={"name": "Permission Test Group"}, headers=owner["headers"]) group_id = grp_res.json()["id"] api_client.post(f"/api/v1/groups/{group_id}/members", json={"email": member["email"]}, headers=owner["headers"]) # Non-owner attempts to delete group -> fails del_bad = api_client.delete(f"/api/v1/groups/{group_id}", headers=member["headers"]) assert del_bad.status_code == 400 # Owner removes member rem_res = api_client.delete(f"/api/v1/groups/{group_id}/members/{member['email']}", headers=owner["headers"]) assert rem_res.status_code == 200 assert member["email"] not in rem_res.json()["memberEmails"] # Member no longer sees group in list of groups member_groups = api_client.get("/api/v1/groups", headers=member["headers"]).json() assert not any(g["id"] == group_id for g in member_groups) # Member cannot add items to group after removal add_item_res = api_client.post( "/api/v1/items", json={"name": "Item After Removal", "lifecycleType": "renewal", "groupId": group_id}, headers=member["headers"], ) assert add_item_res.status_code == 400