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 "behind" in page other = client.get( reverse("features:detail", kwargs={**P, "feature_key": "INT-2"}) ).content.decode() assert "Not started" in other