- Signed-out visitors get the same login redirect for missing and private projects, so the redirect no longer reveals private ones. - Production refuses the console email backend; sign-in codes must never reach the logs. - Database cache shared by all workers for allauth rate limits; invite sending throttled per sender and per recipient. - Email invite links are claimed on first use and the token rotated, so a token copied from request logs is worthless. - allauth password URLs removed; adding an email requires a fresh code and notifies existing addresses. - Markdown drops remote images until uploads exist. - Project and org slugs lowercased; only superusers can grant staff status or permissions in the admin. - Container runs as a non-root user; CI actions pinned to commits. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
140 lines
5.3 KiB
Python
140 lines
5.3 KiB
Python
import pytest
|
|
from django.core.exceptions import PermissionDenied, ValidationError
|
|
from django.urls import reverse
|
|
|
|
from intent.features.models import Feature, FeatureStatus
|
|
from intent.features.services import create_feature, update_feature
|
|
from intent.orgs.models import OrgMembership
|
|
from intent.orgs.services import remove_member as remove_org_member
|
|
from intent.projects import services
|
|
from intent.projects.models import Project, ProjectMembership
|
|
|
|
|
|
def url(name, project, **kwargs):
|
|
return reverse(
|
|
f"projects:{name}",
|
|
kwargs={"org_slug": project.org.slug, "project_slug": project.slug, **kwargs},
|
|
)
|
|
|
|
|
|
def test_create_requires_grant(collaborator, org):
|
|
with pytest.raises(PermissionDenied):
|
|
services.create_project(
|
|
org,
|
|
collaborator,
|
|
slug="x",
|
|
name="X",
|
|
feature_prefix="XX",
|
|
visibility="private",
|
|
open_contribution=False,
|
|
)
|
|
|
|
|
|
def test_granted_collaborator_creates_and_owns_project(client, collaborator, org):
|
|
OrgMembership.objects.filter(user=collaborator).update(can_create_projects=True)
|
|
client.force_login(collaborator)
|
|
response = client.post(
|
|
reverse("project_create", kwargs={"org_slug": org.slug}),
|
|
{"name": "Docs", "slug": "docs", "feature_prefix": "doc", "visibility": "public"},
|
|
)
|
|
project = Project.objects.get(slug="docs")
|
|
assert response.status_code == 302
|
|
assert project.feature_prefix == "DOC"
|
|
assert project.memberships.get(user=collaborator).role == "owner"
|
|
|
|
|
|
def test_duplicate_prefix_is_a_form_error(client, owner, org, project):
|
|
client.force_login(owner)
|
|
response = client.post(
|
|
reverse("project_create", kwargs={"org_slug": org.slug}),
|
|
{"name": "Other", "slug": "other", "feature_prefix": "INT", "visibility": "private"},
|
|
)
|
|
assert response.status_code == 200
|
|
assert b"already exists" in response.content
|
|
|
|
|
|
def test_reserved_slug_rejected(owner, org):
|
|
with pytest.raises(ValidationError):
|
|
services.create_project(
|
|
org,
|
|
owner,
|
|
slug="members",
|
|
name="M",
|
|
feature_prefix="MM",
|
|
visibility="private",
|
|
open_contribution=False,
|
|
)
|
|
|
|
|
|
def test_private_project_hidden_from_outsiders(client, make_user, project):
|
|
missing = f"/{project.org.slug}/no-such-project/"
|
|
# Signed-out visitors can't tell a private project from a missing one: both ask to sign in.
|
|
for path in [url("home", project), missing]:
|
|
response = client.get(path)
|
|
assert response.status_code == 302 and "/accounts/login/" in response["Location"], path
|
|
client.force_login(make_user())
|
|
assert client.get(url("home", project)).status_code == 404
|
|
assert client.get(missing).status_code == 404
|
|
|
|
|
|
def test_public_project_visible_to_anonymous(client, project):
|
|
project.visibility = "public"
|
|
project.save()
|
|
assert client.get(url("home", project)).status_code == 200
|
|
|
|
|
|
def test_settings_require_admin(client, editor, owner, project):
|
|
client.force_login(editor)
|
|
assert client.get(url("settings", project)).status_code == 403
|
|
client.force_login(owner)
|
|
client.post(url("settings", project), {"name": "Renamed", "visibility": "public"})
|
|
project.refresh_from_db()
|
|
assert project.name == "Renamed" and project.visibility == "public"
|
|
|
|
|
|
def test_delete_needs_typed_slug_and_cascades_features(client, owner, project):
|
|
parent = create_feature(project, owner, title="Parent")
|
|
child = create_feature(project, owner, title="Child", parent=parent)
|
|
update_feature(
|
|
parent, owner, note="Replaced", status=FeatureStatus.SUPERSEDED, superseded_by=child
|
|
)
|
|
client.force_login(owner)
|
|
client.post(url("delete", project), {"confirm": "wrong"})
|
|
assert Project.objects.filter(pk=project.pk).exists()
|
|
client.post(url("delete", project), {"confirm": "intent"})
|
|
assert not Project.objects.filter(pk=project.pk).exists()
|
|
assert not Feature.objects.exists()
|
|
|
|
|
|
def test_project_members_must_be_on_the_org_team(owner, make_user, project):
|
|
with pytest.raises(ValidationError):
|
|
services.add_member(project, owner, user=make_user())
|
|
|
|
|
|
def test_leaving_org_drops_project_roles(editor, org, project):
|
|
remove_org_member(org.memberships.get(user=editor), editor)
|
|
assert not ProjectMembership.objects.filter(user=editor).exists()
|
|
|
|
|
|
def test_admin_adds_member_via_page(client, owner, collaborator, project):
|
|
client.force_login(owner)
|
|
client.post(url("members", project), {"user": collaborator.pk, "role": "collaborator"})
|
|
assert project.memberships.filter(user=collaborator).exists()
|
|
|
|
|
|
def test_org_page_lists_visible_projects(client, owner, collaborator, org, project):
|
|
org_page = reverse("orgs:detail", kwargs={"org_slug": org.slug})
|
|
link = f'href="{url("home", project)}"'
|
|
client.force_login(owner)
|
|
assert link in client.get(org_page).content.decode()
|
|
client.force_login(collaborator)
|
|
assert link not in client.get(org_page).content.decode()
|
|
|
|
|
|
def test_slugs_are_lowercased(client, owner, org):
|
|
client.force_login(owner)
|
|
client.post(
|
|
reverse("project_create", kwargs={"org_slug": org.slug}),
|
|
{"name": "Docs", "slug": "Docs", "feature_prefix": "doc", "visibility": "public"},
|
|
)
|
|
assert Project.objects.filter(slug="docs").exists()
|