|
5 | 5 | ``test_cicd_infrastructure_migration.py``. |
6 | 6 | """ |
7 | 7 |
|
| 8 | +from django.core.exceptions import ValidationError |
8 | 9 | from django.db import IntegrityError, transaction |
9 | 10 | from django.urls import reverse |
10 | 11 | from rest_framework.authtoken.models import Token |
@@ -45,6 +46,36 @@ def test_description_and_url_default_to_empty_string(self): |
45 | 46 | self.assertEqual(infra.description, "") |
46 | 47 | self.assertEqual(infra.url, "") |
47 | 48 |
|
| 49 | + def test_save_rejects_infrastructure_type_change_on_existing(self): |
| 50 | + """ |
| 51 | + Model save() guard: infrastructure_type is immutable once an instance exists. |
| 52 | + Engagement CICD FKs are scoped by infrastructure_type via limit_choices_to, |
| 53 | + so flipping the type would leave any referencing engagement pointing at a row |
| 54 | + whose type contradicts the FK's role. |
| 55 | + """ |
| 56 | + infra = CICDInfrastructure.objects.create(name="Locked", infrastructure_type="build_server") |
| 57 | + infra.infrastructure_type = "scm_server" |
| 58 | + with self.assertRaises(ValidationError) as ctx: |
| 59 | + infra.save() |
| 60 | + self.assertIn("infrastructure_type", ctx.exception.message_dict) |
| 61 | + # The DB row is untouched. |
| 62 | + self.assertEqual( |
| 63 | + CICDInfrastructure.objects.get(pk=infra.pk).infrastructure_type, |
| 64 | + "build_server", |
| 65 | + ) |
| 66 | + |
| 67 | + def test_save_allows_changing_other_fields_on_existing(self): |
| 68 | + """name/description/url remain editable post-creation.""" |
| 69 | + infra = CICDInfrastructure.objects.create(name="Editable", infrastructure_type="build_server") |
| 70 | + infra.name = "Renamed" |
| 71 | + infra.description = "now has a description" |
| 72 | + infra.url = "https://renamed.example.com" |
| 73 | + infra.save() |
| 74 | + infra.refresh_from_db() |
| 75 | + self.assertEqual(infra.name, "Renamed") |
| 76 | + self.assertEqual(infra.description, "now has a description") |
| 77 | + self.assertEqual(infra.url, "https://renamed.example.com") |
| 78 | + |
48 | 79 |
|
49 | 80 | # --------------------------------------------------------------------------- |
50 | 81 | # Form |
@@ -167,3 +198,23 @@ def test_non_superuser_cannot_create(self): |
167 | 198 | ) |
168 | 199 | self.assertEqual(r.status_code, 403, r.content[:1000]) |
169 | 200 | self.assertFalse(CICDInfrastructure.objects.filter(name="NonSuperCreate").exists()) |
| 201 | + |
| 202 | + def test_patch_silently_ignores_infrastructure_type_change(self): |
| 203 | + """ |
| 204 | + Serializer marks infrastructure_type read_only on update, so DRF drops the |
| 205 | + incoming value from validated_data. The PATCH succeeds (200) but the type is |
| 206 | + unchanged — same defense the form provides for the legacy UI. |
| 207 | + """ |
| 208 | + existing = CICDInfrastructure.objects.create( |
| 209 | + name="ApiLocked", infrastructure_type="build_server", |
| 210 | + ) |
| 211 | + c = self._client_for("admin") |
| 212 | + r = c.patch( |
| 213 | + reverse("cicd_infrastructure-detail", args=[existing.pk]), |
| 214 | + {"infrastructure_type": "scm_server", "description": "still build"}, |
| 215 | + format="json", |
| 216 | + ) |
| 217 | + self.assertEqual(r.status_code, 200, r.content[:1000]) |
| 218 | + existing.refresh_from_db() |
| 219 | + self.assertEqual(existing.infrastructure_type, "build_server") |
| 220 | + self.assertEqual(existing.description, "still build") |
0 commit comments