intent/tests/test_projects.py
Alec Perkins b9378b8397 Add projects and features with revision history (Phase 2)
Projects under organizations with fixed URL name and feature ID
prefix, three visibility levels, open contribution, per-project
roles, settings and typed-confirmation delete. Hidden projects 404.
Leaving an org drops its project roles. Project access rules live in
intent/access.py alongside org access.

Features numbered per project (INT-12) in a tree. Every change is a
revision with a required note; history, old revisions, and
side-by-side field and line diffs between any two. Cycles and
superseded-without-replacement are rejected. Markdown renders safely
with source-line attributes for Phase 3 line comments and headings
shifted below the page h1. Server-side preview works without JS.

Structural accessibility check runs on every page in tests.

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

127 lines
4.7 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):
response = client.get(url("home", project))
assert response.status_code == 302 and "/accounts/login/" in response["Location"]
client.force_login(make_user())
assert client.get(url("home", project)).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()