intent/tests/test_issues.py

253 lines
10 KiB
Python
Raw Permalink Normal View History

import pytest
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import IntegrityError
from django.urls import reverse
from intent.comments.models import CommentThread, ThreadKind
from intent.features.services import create_feature, update_feature
from intent.issues import services
from intent.issues.models import EventKind, Issue, IssueStatus
def iurl(name, project, **kwargs):
kwargs = {"org_slug": project.org.slug, "project_slug": project.slug, **kwargs}
return reverse(f"issues:{name}", kwargs=kwargs)
@pytest.fixture
def issue(open_project, outsider):
return services.create_issue(open_project, outsider, title="Sign in is slow", body="Why?")
def test_numbers_are_per_project_and_issue_gets_a_thread(open_project, outsider, issue):
second = services.create_issue(open_project, outsider, title="Another")
assert (issue.key, second.key) == ("#1", "#2")
assert CommentThread.objects.get(issue=issue).kind == ThreadKind.ISSUE
def test_outsiders_need_open_contribution(project, outsider):
with pytest.raises(PermissionDenied):
services.create_issue(project, outsider, title="Nope")
project.visibility = "public"
project.save()
with pytest.raises(PermissionDenied):
services.create_issue(project, outsider, title="Still no")
def test_title_required(open_project, outsider):
with pytest.raises(ValidationError):
services.create_issue(open_project, outsider, title=" ")
def test_only_team_decides(issue, outsider, editor):
with pytest.raises(PermissionDenied):
services.set_status(issue, outsider, IssueStatus.ACCEPTED)
services.set_status(issue, editor, IssueStatus.REJECTED)
issue.refresh_from_db()
assert issue.status == IssueStatus.REJECTED and issue.closed_at
services.set_status(issue, editor, IssueStatus.OPEN)
issue.refresh_from_db()
assert issue.is_open and issue.closed_at is None
assert [e.status for e in issue.events.all()] == ["rejected", "open"]
def test_author_withdraws_and_reopens_own_issue(issue, outsider, make_user):
with pytest.raises(PermissionDenied):
services.set_status(issue, make_user(), IssueStatus.WITHDRAWN)
services.set_status(issue, outsider, IssueStatus.WITHDRAWN)
services.set_status(issue, outsider, IssueStatus.OPEN)
services.set_status(issue, outsider, IssueStatus.WITHDRAWN)
issue.refresh_from_db()
assert issue.status == IssueStatus.WITHDRAWN
def test_author_cannot_reopen_a_rejected_issue(issue, outsider, editor):
services.set_status(issue, editor, IssueStatus.REJECTED)
with pytest.raises(PermissionDenied):
services.set_status(issue, outsider, IssueStatus.OPEN)
def test_same_status_is_refused(issue, editor):
with pytest.raises(ValidationError):
services.set_status(issue, editor, IssueStatus.OPEN)
def test_duplicates_need_an_original_and_no_cycles(open_project, outsider, editor, issue):
other = services.create_issue(open_project, outsider, title="Slow sign in")
with pytest.raises(ValidationError):
services.set_status(other, editor, IssueStatus.DUPLICATE)
with pytest.raises(ValidationError):
services.set_status(other, editor, IssueStatus.DUPLICATE, duplicate_of=other)
services.set_status(other, editor, IssueStatus.DUPLICATE, duplicate_of=issue)
with pytest.raises(ValidationError):
services.set_status(issue, editor, IssueStatus.DUPLICATE, duplicate_of=other)
# Reopening clears the link.
services.set_status(other, editor, IssueStatus.OPEN)
other.refresh_from_db()
assert other.duplicate_of is None
def test_database_enforces_duplicate_of(issue):
with pytest.raises(IntegrityError):
Issue.objects.filter(pk=issue.pk).update(status=IssueStatus.DUPLICATE)
def test_edit_keeps_old_text_in_timeline(issue, outsider, make_user):
with pytest.raises(PermissionDenied):
services.update_issue(issue, make_user(), title="Hijack", body="")
services.update_issue(issue, outsider, title="Sign in is very slow", body="Why?")
event = issue.events.get(kind=EventKind.EDITED)
assert event.old_title == "Sign in is slow"
with pytest.raises(ValidationError):
services.update_issue(issue, outsider, title="Sign in is very slow", body="Why?")
def test_link_and_unlink_features(issue, editor, outsider, open_project, owner):
feature = create_feature(open_project, owner, title="Sign in")
with pytest.raises(PermissionDenied):
services.link_feature(issue, outsider, feature)
services.link_feature(issue, editor, feature)
assert list(feature.issues.all()) == [issue]
with pytest.raises(ValidationError):
services.link_feature(issue, editor, feature)
services.unlink_feature(issue, editor, feature)
assert not feature.issues.exists()
def test_feature_revision_can_accept_an_issue(issue, editor, open_project, owner):
feature = create_feature(open_project, owner, title="Sign in")
revision = update_feature(
feature, editor, note="Cache sessions", title="Fast sign in", issues=[issue],
accept_issues=True,
) # fmt: skip
issue.refresh_from_db()
assert issue.status == IssueStatus.ACCEPTED
assert list(revision.issues.all()) == [issue]
assert list(issue.features.all()) == [feature]
assert issue.events.filter(kind=EventKind.REVISION, revision=revision).exists()
def test_cross_project_issue_cannot_be_tied_to_a_feature(issue, org, owner):
from intent.projects.services import create_project
other = create_project(
org, owner, slug="other", name="Other", feature_prefix="OTH", visibility="private",
open_contribution=False,
) # fmt: skip
with pytest.raises(ValidationError):
create_feature(other, owner, title="X", issues=[issue])
# Views
def test_issue_pages_for_outsider_on_open_project(client, open_project, outsider, issue):
client.force_login(outsider)
assert client.get(iurl("list", open_project)).status_code == 200
assert client.get(iurl("create", open_project)).status_code == 200
page = client.get(iurl("detail", open_project, number=1)).content.decode()
assert "Sign in is slow" in page
assert 'value="accepted"' not in page # No decision buttons for outsiders.
assert 'value="withdrawn"' in page
def test_private_issues_are_hidden(client, project, owner, outsider):
issue = services.create_issue(project, owner, title="Secret")
client.force_login(outsider)
assert client.get(iurl("detail", project, number=issue.number)).status_code == 404
def test_create_via_form_with_feature_prefill(client, open_project, outsider, owner):
feature = create_feature(open_project, owner, title="Sign in")
client.force_login(outsider)
page = client.get(iurl("create", open_project) + "?feature=INT-1").content.decode()
assert f'value="{feature.pk}" selected' in page
response = client.post(
iurl("create", open_project),
{"title": "Faster", "body": "Please", "features": [feature.pk]},
)
issue = Issue.objects.get(title="Faster")
assert response["Location"] == iurl("detail", open_project, number=issue.number)
assert list(issue.features.all()) == [feature]
def test_status_buttons(client, open_project, editor, issue):
client.force_login(editor)
client.post(iurl("status", open_project, number=1), {"status": "accepted"})
issue.refresh_from_db()
assert issue.status == IssueStatus.ACCEPTED
def test_duplicate_by_number(client, open_project, editor, outsider, issue):
other = services.create_issue(open_project, outsider, title="Again")
client.force_login(editor)
client.post(iurl("status", open_project, number=2), {"status": "duplicate", "number": 99})
other.refresh_from_db()
assert other.is_open
client.post(iurl("status", open_project, number=2), {"status": "duplicate", "number": 1})
other.refresh_from_db()
assert other.duplicate_of == issue
def test_outsider_cannot_post_status_or_links(client, open_project, outsider, issue, owner):
feature = create_feature(open_project, owner, title="Sign in")
client.force_login(outsider)
assert (
client.post(iurl("status", open_project, number=1), {"status": "accepted"}).status_code
== 403
)
assert (
client.post(
iurl("features", open_project, number=1), {"action": "link", "feature": feature.pk}
).status_code
== 403
)
def test_accept_and_create_feature_from_issue(client, open_project, editor, issue):
client.force_login(editor)
create = reverse("features:create", kwargs={"org_slug": "catbriar", "project_slug": "intent"})
page = client.get(f"{create}?issue=1").content.decode()
assert 'value="Sign in is slow"' in page and "Mark issue #1 accepted" in page
client.post(
f"{create}?issue=1",
{"title": "Fast sign in", "description": "Why?", "note": "", "accept_issue": "on"},
)
issue.refresh_from_db()
feature = issue.features.get()
assert feature.title == "Fast sign in"
assert issue.status == IssueStatus.ACCEPTED
assert feature.revisions.get().issues.get() == issue
def test_update_feature_for_issue_without_accepting(client, open_project, editor, issue, owner):
feature = create_feature(open_project, owner, title="Sign in")
client.force_login(editor)
edit = reverse(
"features:edit",
kwargs={"org_slug": "catbriar", "project_slug": "intent", "feature_key": "INT-1"},
)
page = client.get(f"{edit}?issue=1").content.decode()
assert "#1: Sign in is slow" in page
client.post(
f"{edit}?issue=1",
{"title": "Sign in", "description": "Faster.", "status": "active", "note": "Speed"},
)
issue.refresh_from_db()
assert issue.is_open and list(issue.features.all()) == [feature]
history = reverse(
"features:history",
kwargs={"org_slug": "catbriar", "project_slug": "intent", "feature_key": "INT-1"},
)
assert iurl("detail", open_project, number=1) in client.get(history).content.decode()
def test_list_filters(client, open_project, editor, outsider, issue):
services.create_issue(open_project, outsider, title="Closed one")
services.set_status(Issue.objects.get(number=2), editor, IssueStatus.REJECTED)
client.force_login(outsider)
open_page = client.get(iurl("list", open_project)).content.decode()
closed_page = client.get(iurl("list", open_project) + "?status=closed").content.decode()
assert "Sign in is slow" in open_page and "Closed one" not in open_page
assert "Closed one" in closed_page and "Sign in is slow" not in closed_page