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>
113 lines
4.2 KiB
Python
113 lines
4.2 KiB
Python
import re
|
|
|
|
from django.core import mail
|
|
from django.urls import reverse
|
|
|
|
from intent.orgs.models import InviteStatus, OrgInvite, Role
|
|
|
|
|
|
def members_url(org):
|
|
return reverse("orgs:members", kwargs={"org_slug": org.slug})
|
|
|
|
|
|
def test_org_page_is_public(client, org):
|
|
response = client.get(reverse("orgs:detail", kwargs={"org_slug": org.slug}))
|
|
assert response.status_code == 200
|
|
assert b"Catbriar" in response.content
|
|
|
|
|
|
def test_members_page_requires_membership(client, make_user, org):
|
|
assert client.get(members_url(org)).status_code == 403
|
|
client.force_login(make_user())
|
|
assert client.get(members_url(org)).status_code == 403
|
|
|
|
|
|
def test_collaborator_sees_members_but_cannot_invite(client, collaborator, org):
|
|
client.force_login(collaborator)
|
|
response = client.get(members_url(org))
|
|
assert response.status_code == 200
|
|
assert b"Invite someone" not in response.content
|
|
response = client.post(members_url(org), {"recipient": "x@example.com", "role": "owner"})
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_invite_by_username_then_accept_from_invitations_page(client, owner, make_user, org):
|
|
invitee = make_user("ivan")
|
|
client.force_login(owner)
|
|
response = client.post(members_url(org), {"recipient": "IVAN", "role": "collaborator"})
|
|
assert response.status_code == 302
|
|
invite = OrgInvite.objects.get(user=invitee)
|
|
assert mail.outbox == [] # in-app only; no notifications in V1
|
|
|
|
client.force_login(invitee)
|
|
assert b"Catbriar" in client.get(reverse("invites:list")).content
|
|
response = client.post(
|
|
reverse("invites:respond", kwargs={"invite_id": invite.pk}), {"action": "accept"}
|
|
)
|
|
assert response.status_code == 302
|
|
assert org.memberships.filter(user=invitee, role=Role.COLLABORATOR).exists()
|
|
|
|
|
|
def test_cannot_respond_to_someone_elses_invite(client, owner, make_user, org):
|
|
invite = OrgInvite.objects.create(org=org, user=make_user(), invited_by=owner, token_hash="x")
|
|
client.force_login(make_user())
|
|
response = client.post(
|
|
reverse("invites:respond", kwargs={"invite_id": invite.pk}), {"action": "accept"}
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_email_invite_link_flow(client, owner, make_user, org):
|
|
client.force_login(owner)
|
|
client.post(members_url(org), {"recipient": "new@example.com", "role": "collaborator"})
|
|
assert len(mail.outbox) == 1
|
|
path = re.search(r"https?://[^/]+(/invites/t/\S+/)", mail.outbox[0].body).group(1)
|
|
|
|
client.logout()
|
|
response = client.get(path)
|
|
assert response.status_code == 302
|
|
assert response["Location"].startswith(reverse("account_login"))
|
|
|
|
# Bearer link: any signed-in account can accept, whatever its email.
|
|
joiner = make_user("june", email="june@elsewhere.example")
|
|
client.force_login(joiner)
|
|
assert client.get(path).status_code == 200
|
|
client.post(path, {"action": "accept"})
|
|
assert org.memberships.filter(user=joiner).exists()
|
|
assert OrgInvite.objects.get(email="new@example.com").status == InviteStatus.ACCEPTED
|
|
|
|
|
|
def test_invalid_token_is_404(client, make_user):
|
|
client.force_login(make_user())
|
|
assert client.get(reverse("invites:by_token", kwargs={"token": "nope"})).status_code == 404
|
|
|
|
|
|
def test_owner_updates_role_and_grant(client, owner, collaborator, org):
|
|
membership = org.memberships.get(user=collaborator)
|
|
client.force_login(owner)
|
|
client.post(
|
|
reverse(
|
|
"orgs:member_update", kwargs={"org_slug": org.slug, "membership_id": membership.pk}
|
|
),
|
|
{"role": "collaborator", "can_create_projects": "on"},
|
|
)
|
|
membership.refresh_from_db()
|
|
assert membership.can_create_projects
|
|
|
|
|
|
def test_last_owner_leaving_shows_error(client, owner, org):
|
|
membership = org.memberships.get(user=owner)
|
|
client.force_login(owner)
|
|
response = client.post(
|
|
reverse(
|
|
"orgs:member_remove", kwargs={"org_slug": org.slug, "membership_id": membership.pk}
|
|
),
|
|
follow=True,
|
|
)
|
|
assert b"at least one owner" in response.content
|
|
assert org.memberships.filter(user=owner).exists()
|
|
|
|
|
|
def test_home_lists_orgs_and_invites(client, owner, make_user, org):
|
|
client.force_login(owner)
|
|
assert b"Catbriar" in client.get("/").content
|