intent/tests/test_org_services.py
Alec Perkins 1af217f6fb Add accounts and organizations (Phase 1)
Sign-in via django-allauth: emailed one-time codes, GitHub, Discord,
and Forgejo instances as OpenID Connect apps registered in admin.
Regular users never have a password; sysadmins sign in only at
/admin/ and are redirected there from the normal sign-in.

Organizations at the URL root with owner and collaborator roles, a
per-member project-create grant, and invites by username or email.
Email invites carry a bearer token stored only as a hash. The last
owner can't leave, be removed or be demoted. Permission checks live
in intent/access.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 07:10:27 -04:00

99 lines
3.7 KiB
Python

from datetime import timedelta
import pytest
from django.core.exceptions import PermissionDenied, ValidationError
from django.utils import timezone
from intent.orgs import services
from intent.orgs.models import InviteStatus, OrgMembership, Role, validate_org_slug
def test_create_org_makes_creator_owner(owner, org):
assert org.memberships.get(user=owner).role == Role.OWNER
def test_staff_cannot_own_org(make_user):
with pytest.raises(ValidationError):
services.create_org(slug="x", name="X", owner=make_user(is_staff=True))
def test_reserved_slug_rejected():
with pytest.raises(ValidationError):
validate_org_slug("Admin")
def test_invite_requires_owner(collaborator, make_user, org):
with pytest.raises(PermissionDenied):
services.invite(org, collaborator, user=make_user())
def test_reinvite_replaces_pending_invite(owner, make_user, org):
invitee = make_user()
first, _ = services.invite(org, owner, user=invitee)
second, _ = services.invite(org, owner, user=invitee, role=Role.OWNER)
first.refresh_from_db()
assert first.status == InviteStatus.REVOKED
assert second.status == InviteStatus.PENDING
def test_cannot_invite_existing_member_or_staff(owner, collaborator, make_user, org):
with pytest.raises(ValidationError):
services.invite(org, owner, user=collaborator)
with pytest.raises(ValidationError):
services.invite(org, owner, email="COLE@example.com")
with pytest.raises(ValidationError):
services.invite(org, owner, user=make_user(is_staff=True))
def test_accept_creates_membership_with_invited_role(owner, make_user, org):
invitee = make_user()
invite, _ = services.invite(org, owner, user=invitee, role=Role.OWNER)
services.accept_invite(invite, invitee)
assert org.memberships.get(user=invitee).role == Role.OWNER
invite.refresh_from_db()
assert invite.status == InviteStatus.ACCEPTED
assert invite.accepted_by == invitee
def test_expired_invite_cannot_be_accepted(owner, make_user, org):
invitee = make_user()
invite, _ = services.invite(org, owner, user=invitee)
invite.expires_at = timezone.now() - timedelta(seconds=1)
invite.save()
with pytest.raises(ValidationError):
services.accept_invite(invite, invitee)
def test_email_invite_is_found_by_verified_email(owner, make_user, org):
invitee = make_user("ivy")
invite, _ = services.invite(org, owner, email="Ivy@Example.com")
assert list(services.open_invites_for(invitee)) == [invite]
assert services.is_addressed_to(invite, invitee)
assert not services.is_addressed_to(invite, make_user())
def test_last_owner_cannot_leave_or_be_demoted(owner, org):
membership = org.memberships.get(user=owner)
with pytest.raises(ValidationError):
services.remove_member(membership, owner)
with pytest.raises(ValidationError):
services.update_membership(
membership, owner, role=Role.COLLABORATOR, can_create_projects=False
)
def test_owner_can_step_down_once_another_owner_exists(owner, collaborator, org):
services.update_membership(
org.memberships.get(user=collaborator), owner, role=Role.OWNER, can_create_projects=False
)
services.remove_member(org.memberships.get(user=owner), owner)
assert not org.memberships.filter(user=owner).exists()
def test_member_can_leave_but_not_remove_others(owner, collaborator, make_user, org):
other = make_user()
OrgMembership.objects.create(org=org, user=other)
with pytest.raises(PermissionDenied):
services.remove_member(org.memberships.get(user=other), collaborator)
services.remove_member(org.memberships.get(user=collaborator), collaborator)
assert not org.memberships.filter(user=collaborator).exists()