@@ -390,6 +390,144 @@ class TestInviteEndpoints:
390390 )
391391
392392 assert response.status_code == status.HTTP_400_BAD_REQUEST
393+
394+
395+ @pytest.mark.usefixtures("_enable_auth")
396+ class TestMemberDetails:
397+ """Test GET /orgs/{org_id}/members/details."""
398+
399+ @pytest.mark.asyncio
400+ async def test_org_admin_can_get_member_details(
401+ self, async_client_with_db: TestClient, async_db_session: AsyncSession,
402+ setup_org_with_owner, create_user,
403+ ) -> None:
404+ """Org admin can see member emails and names."""
405+ _, org, token = await setup_org_with_owner(
406+ email="det-owner@test.com", slug="det-org"
407+ )
408+ member = await create_user("det-member@test.com")
409+ membership_service = MembershipService(async_db_session)
410+ await membership_service.add_member(org.id, member.id)
411+
412+ response = async_client_with_db.get(
413+ f"/api/v1/orgs/{org.id}/members/details",
414+ headers={"Authorization": f"Bearer {token}"},
415+ )
416+
417+ assert response.status_code == status.HTTP_200_OK
418+ data = response.json()
419+ assert len(data) >= 2
420+ emails = {m["email"] for m in data}
421+ assert "det-member@test.com" in emails
422+
423+ @pytest.mark.asyncio
424+ async def test_non_member_cannot_get_member_details(
425+ self, async_client_with_db: TestClient, async_db_session: AsyncSession,
426+ setup_org_with_owner, create_user_with_token,
427+ ) -> None:
428+ """Non-member cannot see org member details."""
429+ _, org, _ = await setup_org_with_owner(
430+ email="det2-owner@test.com", slug="det2-org"
431+ )
432+ _, outsider_token = await create_user_with_token("det2-outsider@test.com")
433+
434+ response = async_client_with_db.get(
435+ f"/api/v1/orgs/{org.id}/members/details",
436+ headers={"Authorization": f"Bearer {outsider_token}"},
437+ )
438+
439+ assert response.status_code == status.HTTP_403_FORBIDDEN
440+
441+
442+ @pytest.mark.usefixtures("_enable_auth")
443+ class TestOwnershipTransfer:
444+ """Test POST /orgs/{org_id}/transfer-ownership."""
445+
446+ @pytest.mark.asyncio
447+ async def test_owner_can_transfer(
448+ self, async_client_with_db: TestClient, async_db_session: AsyncSession,
449+ setup_org_with_owner, create_user,
450+ ) -> None:
451+ """Owner transfers ownership to an admin member."""
452+ owner, org, token = await setup_org_with_owner(
453+ email="xfer-owner@test.com", slug="xfer-org"
454+ )
455+ admin = await create_user("xfer-admin@test.com")
456+ membership_service = MembershipService(async_db_session)
457+ await membership_service.add_member(org.id, admin.id, role="admin")
458+
459+ response = async_client_with_db.post(
460+ f"/api/v1/orgs/{org.id}/transfer-ownership",
461+ json={"user_id": admin.id},
462+ headers={"Authorization": f"Bearer {token}"},
463+ )
464+
465+ assert response.status_code == status.HTTP_200_OK
466+
467+ new_owner = await membership_service.get_member(org.id, admin.id)
468+ assert new_owner.role == "owner"
469+
470+ old_owner = await membership_service.get_member(org.id, owner.id)
471+ assert old_owner.role == "admin"
472+
473+ @pytest.mark.asyncio
474+ async def test_non_owner_cannot_transfer(
475+ self, async_client_with_db: TestClient, async_db_session: AsyncSession,
476+ setup_org_with_owner, create_user_with_token,
477+ ) -> None:
478+ """Admin cannot transfer ownership."""
479+ _, org, _ = await setup_org_with_owner(
480+ email="xfer2-owner@test.com", slug="xfer2-org"
481+ )
482+ admin, admin_token = await create_user_with_token("xfer2-admin@test.com")
483+ membership_service = MembershipService(async_db_session)
484+ await membership_service.add_member(org.id, admin.id, role="admin")
485+
486+ response = async_client_with_db.post(
487+ f"/api/v1/orgs/{org.id}/transfer-ownership",
488+ json={"user_id": admin.id},
489+ headers={"Authorization": f"Bearer {admin_token}"},
490+ )
491+
492+ assert response.status_code == status.HTTP_403_FORBIDDEN
493+
494+ @pytest.mark.asyncio
495+ async def test_transfer_to_non_member_fails(
496+ self, async_client_with_db: TestClient, async_db_session: AsyncSession,
497+ setup_org_with_owner, create_user,
498+ ) -> None:
499+ """Cannot transfer to someone who isn't a member."""
500+ _, org, token = await setup_org_with_owner(
501+ email="xfer3-owner@test.com", slug="xfer3-org"
502+ )
503+ outsider = await create_user("xfer3-outsider@test.com")
504+
505+ response = async_client_with_db.post(
506+ f"/api/v1/orgs/{org.id}/transfer-ownership",
507+ json={"user_id": outsider.id},
508+ headers={"Authorization": f"Bearer {token}"},
509+ )
510+
511+ assert response.status_code == status.HTTP_400_BAD_REQUEST
512+
513+ @pytest.mark.asyncio
514+ async def test_self_transfer_fails(
515+ self, async_client_with_db: TestClient, async_db_session: AsyncSession,
516+ setup_org_with_owner,
517+ ) -> None:
518+ """Cannot transfer ownership to yourself."""
519+ owner, org, token = await setup_org_with_owner(
520+ email="xfer4-owner@test.com", slug="xfer4-org"
521+ )
522+
523+ response = async_client_with_db.post(
524+ f"/api/v1/orgs/{org.id}/transfer-ownership",
525+ json={"user_id": owner.id},
526+ headers={"Authorization": f"Bearer {token}"},
527+ )
528+
529+ assert response.status_code == status.HTTP_400_BAD_REQUEST
530+ assert "yourself" in response.json()["detail"].lower()
393531{% else %}
394532# Organization endpoint tests not included (auth_level != org)
395533{% endif %}
0 commit comments