|
| 1 | +"""Tests for the CICDInfrastructure model, form, and API. |
| 2 | +
|
| 3 | +Migration behaviour is tested separately in |
| 4 | +``test_cicd_infrastructure_migration.py``. |
| 5 | +""" |
| 6 | + |
| 7 | +from django.db import IntegrityError, transaction |
| 8 | +from django.urls import reverse |
| 9 | +from rest_framework.authtoken.models import Token |
| 10 | +from rest_framework.test import APIClient, APITestCase |
| 11 | + |
| 12 | +from dojo.forms import CICDInfrastructureForm |
| 13 | +from dojo.models import CICDInfrastructure |
| 14 | + |
| 15 | + |
| 16 | +# --------------------------------------------------------------------------- |
| 17 | +# Model |
| 18 | +# --------------------------------------------------------------------------- |
| 19 | + |
| 20 | +class CICDInfrastructureModelTests(APITestCase): |
| 21 | + """Model-level constraints and defaults.""" |
| 22 | + |
| 23 | + def test_unique_together_within_same_type_rejects_duplicate(self): |
| 24 | + CICDInfrastructure.objects.create(name="Jenkins", infrastructure_type="build_server") |
| 25 | + with self.assertRaises(IntegrityError): |
| 26 | + with transaction.atomic(): |
| 27 | + CICDInfrastructure.objects.create(name="Jenkins", infrastructure_type="build_server") |
| 28 | + |
| 29 | + def test_same_name_across_different_types_allowed(self): |
| 30 | + """unique_together is (name, infrastructure_type), so the same name across |
| 31 | + different types coexists — supports the 'one Jenkins instance, multiple |
| 32 | + roles' case.""" |
| 33 | + CICDInfrastructure.objects.create(name="Jenkins", infrastructure_type="build_server") |
| 34 | + CICDInfrastructure.objects.create(name="Jenkins", infrastructure_type="scm_server") |
| 35 | + CICDInfrastructure.objects.create(name="Jenkins", infrastructure_type="orchestration") |
| 36 | + self.assertEqual(CICDInfrastructure.objects.filter(name="Jenkins").count(), 3) |
| 37 | + |
| 38 | + def test_description_and_url_default_to_empty_string(self): |
| 39 | + """description and url are non-null with default='' (no None values).""" |
| 40 | + infra = CICDInfrastructure.objects.create(name="Bare", infrastructure_type="build_server") |
| 41 | + self.assertEqual(infra.description, "") |
| 42 | + self.assertEqual(infra.url, "") |
| 43 | + |
| 44 | + |
| 45 | +# --------------------------------------------------------------------------- |
| 46 | +# Form |
| 47 | +# --------------------------------------------------------------------------- |
| 48 | + |
| 49 | +class CICDInfrastructureFormTests(APITestCase): |
| 50 | + """Form-level behaviour — specifically the type-locked-on-edit rule.""" |
| 51 | + |
| 52 | + def test_infrastructure_type_editable_on_create(self): |
| 53 | + form = CICDInfrastructureForm() |
| 54 | + self.assertFalse(form.fields["infrastructure_type"].disabled) |
| 55 | + |
| 56 | + def test_infrastructure_type_disabled_on_edit(self): |
| 57 | + existing = CICDInfrastructure.objects.create( |
| 58 | + name="LockedType", infrastructure_type="build_server", |
| 59 | + ) |
| 60 | + form = CICDInfrastructureForm(instance=existing) |
| 61 | + self.assertTrue(form.fields["infrastructure_type"].disabled) |
| 62 | + |
| 63 | + def test_disabled_field_ignores_posted_value(self): |
| 64 | + """Django enforces ``disabled=True`` server-side — POSTed values |
| 65 | + for the type field on edit are silently ignored, so users cannot |
| 66 | + sneak around the UI lock.""" |
| 67 | + existing = CICDInfrastructure.objects.create( |
| 68 | + name="LockedType2", infrastructure_type="build_server", |
| 69 | + ) |
| 70 | + form = CICDInfrastructureForm( |
| 71 | + data={ |
| 72 | + "name": "LockedType2", |
| 73 | + "description": "", |
| 74 | + "url": "", |
| 75 | + "infrastructure_type": "orchestration", |
| 76 | + }, |
| 77 | + instance=existing, |
| 78 | + ) |
| 79 | + self.assertTrue(form.is_valid(), form.errors) |
| 80 | + saved = form.save() |
| 81 | + self.assertEqual(saved.infrastructure_type, "build_server") |
| 82 | + |
| 83 | + |
| 84 | +# --------------------------------------------------------------------------- |
| 85 | +# API |
| 86 | +# --------------------------------------------------------------------------- |
| 87 | + |
| 88 | +class CICDInfrastructureAPITests(APITestCase): |
| 89 | + """The /cicd_infrastructure endpoint uses UserHasConfigurationPermissionSuperuser |
| 90 | + — superusers can do anything; non-superusers need the explicit Django permission.""" |
| 91 | + |
| 92 | + fixtures = ["dojo_testdata.json"] |
| 93 | + |
| 94 | + def _client_for(self, username): |
| 95 | + token = Token.objects.get(user__username=username) |
| 96 | + c = APIClient() |
| 97 | + c.credentials(HTTP_AUTHORIZATION="Token " + token.key) |
| 98 | + return c |
| 99 | + |
| 100 | + def test_superuser_can_list(self): |
| 101 | + c = self._client_for("admin") |
| 102 | + r = c.get(reverse("cicd_infrastructure-list")) |
| 103 | + self.assertEqual(r.status_code, 200, r.content[:1000]) |
| 104 | + |
| 105 | + def test_superuser_can_create(self): |
| 106 | + c = self._client_for("admin") |
| 107 | + r = c.post( |
| 108 | + reverse("cicd_infrastructure-list"), |
| 109 | + {"name": "Created via API", "infrastructure_type": "build_server"}, |
| 110 | + format="json", |
| 111 | + ) |
| 112 | + self.assertEqual(r.status_code, 201, r.content[:1000]) |
| 113 | + self.assertTrue(CICDInfrastructure.objects.filter(name="Created via API").exists()) |
| 114 | + |
| 115 | + def test_create_rejects_duplicate_name_same_type(self): |
| 116 | + c = self._client_for("admin") |
| 117 | + CICDInfrastructure.objects.create(name="ApiDupe", infrastructure_type="scm_server") |
| 118 | + r = c.post( |
| 119 | + reverse("cicd_infrastructure-list"), |
| 120 | + {"name": "ApiDupe", "infrastructure_type": "scm_server"}, |
| 121 | + format="json", |
| 122 | + ) |
| 123 | + # DRF translates the unique_together violation to 400. |
| 124 | + self.assertEqual(r.status_code, 400, r.content[:1000]) |
| 125 | + |
| 126 | + def test_create_allows_same_name_different_type(self): |
| 127 | + c = self._client_for("admin") |
| 128 | + CICDInfrastructure.objects.create(name="Shared", infrastructure_type="build_server") |
| 129 | + r = c.post( |
| 130 | + reverse("cicd_infrastructure-list"), |
| 131 | + {"name": "Shared", "infrastructure_type": "orchestration"}, |
| 132 | + format="json", |
| 133 | + ) |
| 134 | + self.assertEqual(r.status_code, 201, r.content[:1000]) |
| 135 | + |
| 136 | + def test_non_superuser_is_rejected(self): |
| 137 | + # 'user1' (pk=2 in dojo_testdata.json) is a non-superuser with a token. |
| 138 | + try: |
| 139 | + c = self._client_for("user1") |
| 140 | + except Token.DoesNotExist: |
| 141 | + self.skipTest("user1 token not present in dojo_testdata fixture.") |
| 142 | + r = c.get(reverse("cicd_infrastructure-list")) |
| 143 | + self.assertIn(r.status_code, (401, 403), f"expected 401/403, got {r.status_code}") |
0 commit comments