129 lines
5 KiB
Python
129 lines
5 KiB
Python
|
|
import pytest
|
||
|
|
from django.core.exceptions import PermissionDenied, ValidationError
|
||
|
|
from django.urls import reverse
|
||
|
|
|
||
|
|
from intent.features.services import create_feature, update_feature
|
||
|
|
from intent.implementations import services
|
||
|
|
from intent.implementations.models import FeatureImplStatus, FeatureStatus
|
||
|
|
|
||
|
|
P = {"org_slug": "catbriar", "project_slug": "intent"}
|
||
|
|
|
||
|
|
|
||
|
|
def iurl(name, impl=None):
|
||
|
|
kwargs = dict(P)
|
||
|
|
if impl is not None:
|
||
|
|
kwargs["impl_id"] = impl.pk
|
||
|
|
return reverse(f"implementations:{name}", kwargs=kwargs)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def impl(open_project, owner):
|
||
|
|
return services.create_implementation(
|
||
|
|
open_project, owner, name="Web app", url="https://forge.example/acme/web"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def features(open_project, owner):
|
||
|
|
return [create_feature(open_project, owner, title=t) for t in ("Sign in", "Sign out")]
|
||
|
|
|
||
|
|
|
||
|
|
def test_only_editors_create_and_urls_must_be_http(open_project, collaborator, owner, impl):
|
||
|
|
with pytest.raises(PermissionDenied):
|
||
|
|
services.create_implementation(
|
||
|
|
open_project, collaborator, name="X", url="https://x.example"
|
||
|
|
)
|
||
|
|
with pytest.raises(ValidationError):
|
||
|
|
services.create_implementation(open_project, owner, name="X", url="javascript:alert(1)")
|
||
|
|
with pytest.raises(ValidationError, match="already"):
|
||
|
|
services.create_implementation(open_project, owner, name="web APP", url="https://x.example")
|
||
|
|
|
||
|
|
|
||
|
|
def test_status_is_stamped_and_goes_behind(impl, features, owner, editor):
|
||
|
|
sign_in, sign_out = features
|
||
|
|
changed = services.set_statuses(
|
||
|
|
impl,
|
||
|
|
editor,
|
||
|
|
{sign_in: (FeatureStatus.SHIPPED, False), sign_out: (FeatureStatus.NOT_STARTED, False)},
|
||
|
|
)
|
||
|
|
assert changed == 1 # "Not started" is the default, so no row is written for it.
|
||
|
|
row = FeatureImplStatus.objects.get()
|
||
|
|
assert (row.status, row.feature_revision, row.is_behind) == ("shipped", 1, False)
|
||
|
|
|
||
|
|
update_feature(sign_in, owner, note="Add 2FA", description="Now with 2FA")
|
||
|
|
row.refresh_from_db()
|
||
|
|
assert row.is_behind
|
||
|
|
|
||
|
|
# Unchanged and unconfirmed: nothing happens. Confirmed: re-stamped at the current revision.
|
||
|
|
sign_in.refresh_from_db()
|
||
|
|
assert services.set_statuses(impl, editor, {sign_in: ("shipped", False)}) == 0
|
||
|
|
assert services.set_statuses(impl, editor, {sign_in: ("shipped", True)}) == 1
|
||
|
|
row.refresh_from_db()
|
||
|
|
assert (row.feature_revision, row.is_behind) == (2, False)
|
||
|
|
|
||
|
|
|
||
|
|
def test_outsiders_cannot_set_statuses(impl, features, outsider):
|
||
|
|
with pytest.raises(PermissionDenied):
|
||
|
|
services.set_statuses(impl, outsider, {features[0]: ("shipped", False)})
|
||
|
|
|
||
|
|
|
||
|
|
# Views
|
||
|
|
|
||
|
|
|
||
|
|
def test_status_grid_via_form(client, impl, features, editor):
|
||
|
|
sign_in, sign_out = features
|
||
|
|
client.force_login(editor)
|
||
|
|
client.post(
|
||
|
|
iurl("statuses", impl),
|
||
|
|
{f"status-{sign_in.pk}": "shipped", f"status-{sign_out.pk}": "in_progress"},
|
||
|
|
)
|
||
|
|
statuses = dict(impl.feature_statuses.values_list("feature__number", "status"))
|
||
|
|
assert statuses == {1: "shipped", 2: "in_progress"}
|
||
|
|
|
||
|
|
|
||
|
|
def test_bad_status_value_is_refused(client, impl, features, editor):
|
||
|
|
client.force_login(editor)
|
||
|
|
client.post(iurl("statuses", impl), {f"status-{features[0].pk}": "done"})
|
||
|
|
assert not impl.feature_statuses.exists()
|
||
|
|
|
||
|
|
|
||
|
|
def test_outsider_sees_grid_read_only(client, impl, features, outsider):
|
||
|
|
client.force_login(outsider)
|
||
|
|
page = client.get(iurl("detail", impl)).content.decode()
|
||
|
|
assert "Sign in" in page and 'id="status-' not in page
|
||
|
|
assert client.post(iurl("statuses", impl), {}).status_code == 403
|
||
|
|
assert client.get(iurl("create")).status_code == 403
|
||
|
|
|
||
|
|
|
||
|
|
def test_create_edit_archive_delete(client, open_project, owner, editor):
|
||
|
|
client.force_login(editor)
|
||
|
|
client.post(iurl("create"), {"name": "CLI", "url": "https://forge.example/acme/cli"})
|
||
|
|
impl = open_project.implementations.get()
|
||
|
|
client.post(
|
||
|
|
iurl("edit", impl),
|
||
|
|
{"name": "CLI", "url": "https://forge.example/acme/cli", "status": "archived"},
|
||
|
|
)
|
||
|
|
impl.refresh_from_db()
|
||
|
|
assert impl.status == "archived"
|
||
|
|
assert "Archived" in client.get(iurl("list")).content.decode()
|
||
|
|
assert client.post(iurl("delete", impl), {"confirm": "CLI"}).status_code == 403
|
||
|
|
client.force_login(owner)
|
||
|
|
client.post(iurl("delete", impl), {"confirm": "cli"})
|
||
|
|
assert open_project.implementations.exists()
|
||
|
|
client.post(iurl("delete", impl), {"confirm": "CLI"})
|
||
|
|
assert not open_project.implementations.exists()
|
||
|
|
|
||
|
|
|
||
|
|
def test_feature_page_shows_each_implementation(client, impl, features, owner, editor):
|
||
|
|
services.set_statuses(impl, editor, {features[0]: ("shipped", False)})
|
||
|
|
update_feature(features[0], owner, note="Change", title="Sign in v2")
|
||
|
|
client.force_login(editor)
|
||
|
|
page = client.get(
|
||
|
|
reverse("features:detail", kwargs={**P, "feature_key": "INT-1"})
|
||
|
|
).content.decode()
|
||
|
|
assert "Web app" in page and "Shipped" in page and "<strong>behind</strong>" in page
|
||
|
|
other = client.get(
|
||
|
|
reverse("features:detail", kwargs={**P, "feature_key": "INT-2"})
|
||
|
|
).content.decode()
|
||
|
|
assert "Not started" in other
|