Skip to content

Commit 3537593

Browse files
committed
[tests] Add API test for VpnClient partial deletion failure openwisp#1221
Covers the except-Exception path in the serializer's per-instance VpnClient deletion, closing the coverage gap introduced by efeefce.
1 parent efeefce commit 3537593

File tree

1 file changed

+61
-0
lines changed

1 file changed

+61
-0
lines changed

openwisp_controller/config/tests/test_api.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,6 +1595,67 @@ def test_device_detail_api_change_config(self):
15951595
self.assertEqual(response.status_code, 200)
15961596
self.assertEqual(device.config.templates.count(), 0)
15971597

1598+
def test_vpnclient_delete_partial_failure_on_api_template_change(self):
1599+
"""Regression test for #1221: if one VpnClient deletion fails
1600+
during API template update, remaining VpnClients should still
1601+
be deleted and the API should not crash."""
1602+
org = self._get_org()
1603+
vpn1 = self._create_vpn(name="vpn1", organization=org)
1604+
vpn2 = self._create_vpn(name="vpn2", organization=org)
1605+
t1 = self._create_template(
1606+
name="vpn-test-1", type="vpn", vpn=vpn1, organization=org, auto_cert=True
1607+
)
1608+
t2 = self._create_template(
1609+
name="vpn-test-2", type="vpn", vpn=vpn2, organization=org, auto_cert=True
1610+
)
1611+
generic_template = self._create_template(organization=org)
1612+
device = self._create_device(organization=org)
1613+
config = self._create_config(device=device)
1614+
path = reverse("config_api:device_detail", args=[device.pk])
1615+
# Add both VPN templates
1616+
data = {
1617+
"name": device.name,
1618+
"organization": str(org.id),
1619+
"mac_address": device.mac_address,
1620+
"config": {
1621+
"backend": "netjsonconfig.OpenWrt",
1622+
"templates": [str(t1.pk), str(t2.pk), str(generic_template.pk)],
1623+
},
1624+
}
1625+
response = self.client.put(path, data, content_type="application/json")
1626+
self.assertEqual(response.status_code, 200)
1627+
self.assertEqual(config.vpnclient_set.count(), 2)
1628+
vc1, vc2 = list(config.vpnclient_set.order_by("pk"))
1629+
vc1_pk = vc1.pk
1630+
vc2_pk = vc2.pk
1631+
1632+
original_delete = VpnClient.delete
1633+
call_count = 0
1634+
1635+
def failing_delete(self):
1636+
nonlocal call_count
1637+
call_count += 1
1638+
if call_count == 1:
1639+
raise Exception("simulated failure")
1640+
return original_delete(self)
1641+
1642+
# Remove both VPN templates, keep only generic
1643+
data["config"]["templates"] = [str(generic_template.pk)]
1644+
with patch.object(VpnClient, "delete", failing_delete):
1645+
with self.assertLogs(
1646+
"openwisp_controller.config.api.serializers", level="ERROR"
1647+
) as log_cm:
1648+
response = self.client.put(path, data, content_type="application/json")
1649+
self.assertEqual(response.status_code, 200)
1650+
# First VpnClient delete failed, so it should still exist
1651+
self.assertTrue(VpnClient.objects.filter(pk=vc1_pk).exists())
1652+
# Second VpnClient should be deleted
1653+
self.assertFalse(VpnClient.objects.filter(pk=vc2_pk).exists())
1654+
self.assertTrue(
1655+
any(str(vc1_pk) in msg for msg in log_cm.output),
1656+
f"Expected VpnClient PK {vc1_pk} in log output",
1657+
)
1658+
15981659
def test_multiple_vpn_client_templates_same_vpn(self):
15991660
"""
16001661
Assigning multiple templates of type 'vpn' referencing the same VPN

0 commit comments

Comments
 (0)