Skip to content

Commit 8e25060

Browse files
author
prakash-kalwaniya
committed
[tests] Fix on_commit test failures and add regression tests
- Wrap device creation and HTTP requests with captureOnCommitCallbacks(execute=True) in TestCase-based tests so transaction.on_commit callbacks fire correctly (TestCase wraps tests in a never-committed transaction so on_commit never fires) - Affected tests: test_config_backend_changed, test_device_create_with_group, test_device_api_change_config_backend, test_add_device_with_group_templates, test_device_import_with_group_apply_templates, test_unassigning_group_removes_old_templates, test_group_templates_are_not_forced - Add test_handlers_transaction.py with TransactionTestCase to verify change_devices_templates.delay is called after commit and not on rollback for all three model handlers (Device, DeviceGroup, Config) and the list branch
1 parent 2ec3a9c commit 8e25060

File tree

4 files changed

+135
-14
lines changed

4 files changed

+135
-14
lines changed

openwisp_controller/config/tests/test_admin.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -671,9 +671,12 @@ def test_device_import_with_group_apply_templates(self):
671671
self.assertIn("confirm_form", response.context)
672672
confirm_form = response.context["confirm_form"]
673673
data = confirm_form.initial
674-
response = self.client.post(
675-
reverse(f"admin:{self.app_label}_device_process_import"), data, follow=True
676-
)
674+
with self.captureOnCommitCallbacks(execute=True):
675+
response = self.client.post(
676+
reverse(f"admin:{self.app_label}_device_process_import"),
677+
data,
678+
follow=True,
679+
)
677680
self.assertEqual(response.status_code, 200)
678681
device = Device.objects.first()
679682
self.assertIsNotNone(device)
@@ -746,9 +749,10 @@ def test_add_device_with_group_templates(self):
746749
with catch_signal(post_save) as mock_post_save, catch_signal(
747750
device_group_changed
748751
) as device_group_changed_mock:
749-
self.client.post(
750-
reverse(f"admin:{self.app_label}_device_add"), data, follow=True
751-
)
752+
with self.captureOnCommitCallbacks(execute=True):
753+
self.client.post(
754+
reverse(f"admin:{self.app_label}_device_add"), data, follow=True
755+
)
752756
device = Device.objects.first()
753757
mock_post_save.assert_any_call(
754758
signal=post_save,
@@ -767,7 +771,8 @@ def test_unassigning_group_removes_old_templates(self):
767771
template = self._create_template(name="template")
768772
dg = self._create_device_group(name="test-group", organization=org)
769773
dg.templates.add(template)
770-
device = self._create_device(organization=org, group=dg)
774+
with self.captureOnCommitCallbacks(execute=True):
775+
device = self._create_device(organization=org, group=dg)
771776
self.assertIn(template, device.config.templates.all())
772777
path = reverse(f"admin:{self.app_label}_device_change", args=[device.pk])
773778
params = self._get_device_params(org=org)
@@ -780,7 +785,8 @@ def test_unassigning_group_removes_old_templates(self):
780785
"group": "",
781786
}
782787
)
783-
response = self.client.post(path, params, follow=True)
788+
with self.captureOnCommitCallbacks(execute=True):
789+
response = self.client.post(path, params, follow=True)
784790
self.assertNotContains(response, "errors", status_code=200)
785791
device.refresh_from_db()
786792
self.assertIsNone(device.group)
@@ -791,7 +797,8 @@ def test_group_templates_are_not_forced(self):
791797
t = self._create_template(name="t")
792798
dg = self._create_device_group(name="test-group", organization=o)
793799
dg.templates.add(t)
794-
d = self._create_device(organization=o, group=dg)
800+
with self.captureOnCommitCallbacks(execute=True):
801+
d = self._create_device(organization=o, group=dg)
795802
self.assertIn(t, d.config.templates.all())
796803
path = reverse(f"admin:{self.app_label}_device_change", args=[d.pk])
797804
params = self._get_device_params(org=o)

openwisp_controller/config/tests/test_api.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,8 @@ def test_device_create_with_group(self):
172172
org = self._get_org()
173173
data["organization"] = org.pk
174174
data["group"] = dg.pk
175-
r = self.client.post(path, data, content_type="application/json")
175+
with self.captureOnCommitCallbacks(execute=True):
176+
r = self.client.post(path, data, content_type="application/json")
176177
self.assertEqual(r.status_code, 201)
177178
self.assertEqual(Device.objects.count(), 1)
178179
self.assertIn(template, Device.objects.first().config.templates.all())
@@ -474,7 +475,8 @@ def test_device_api_change_config_backend(self):
474475
t2 = self._create_template(name="t2", backend="netjsonconfig.OpenWisp")
475476
dg1 = self._create_device_group(name="dg-1")
476477
dg1.templates.add(t1, t2)
477-
d1 = self._create_device(name="test-device", group=dg1)
478+
with self.captureOnCommitCallbacks(execute=True):
479+
d1 = self._create_device(name="test-device", group=dg1)
478480
self.assertIn(t1, d1.config.templates.all())
479481
path = reverse("config_api:device_detail", args=[d1.pk])
480482
data = {
@@ -490,7 +492,8 @@ def test_device_api_change_config_backend(self):
490492
"config": {},
491493
},
492494
}
493-
r = self.client.put(path, data, content_type="application/json")
495+
with self.captureOnCommitCallbacks(execute=True):
496+
r = self.client.put(path, data, content_type="application/json")
494497
self.assertEqual(r.status_code, 200)
495498
self.assertEqual(r.data["group"], dg1.pk)
496499
self.assertEqual(r.data["config"]["backend"], "netjsonconfig.OpenWisp")

openwisp_controller/config/tests/test_config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,8 @@ def test_config_backend_changed(self):
878878
group.templates.add(*[t1, t2])
879879
with self.subTest("config_backend_changed signal must not be sent on creation"):
880880
with catch_signal(config_backend_changed) as handler:
881-
d = self._create_device(group=group, organization=org)
881+
with self.captureOnCommitCallbacks(execute=True):
882+
d = self._create_device(group=group, organization=org)
882883
handler.assert_not_called()
883884
self.assertTrue(d.config.templates.filter(pk=t1.pk).exists())
884885
self.assertFalse(d.config.templates.filter(pk=t2.pk).exists())
@@ -896,7 +897,8 @@ def test_config_backend_changed(self):
896897
with catch_signal(config_backend_changed) as handler:
897898
c = d.config
898899
c.backend = backend
899-
c.save(update_fields=["backend"])
900+
with self.captureOnCommitCallbacks(execute=True):
901+
c.save(update_fields=["backend"])
900902
handler.assert_called_once_with(
901903
sender=Config,
902904
signal=config_backend_changed,
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from unittest.mock import patch
2+
3+
from django.db import transaction
4+
from django.test import TransactionTestCase
5+
from swapper import load_model
6+
7+
from openwisp_controller.config.handlers import devicegroup_templates_change_handler
8+
9+
Device = load_model("config", "Device")
10+
DeviceGroup = load_model("config", "DeviceGroup")
11+
Config = load_model("config", "Config")
12+
13+
14+
class ForceRollback(Exception):
15+
"""Sentinel exception used to deliberately roll back a transaction in tests."""
16+
17+
18+
class TestHandlersTransaction(TransactionTestCase):
19+
@patch("openwisp_controller.config.tasks.change_devices_templates.delay")
20+
def test_devicegroup_templates_change_handler_on_commit(self, mock_delay):
21+
"""
22+
Test that change_devices_templates is called via delay and only after
23+
transaction.on_commit block commits successfully.
24+
"""
25+
26+
class MockMeta:
27+
def __init__(self, model_name):
28+
self.model_name = model_name
29+
30+
class MockState:
31+
def __init__(self, adding):
32+
self.adding = adding
33+
34+
class MockInstance:
35+
def __init__(self, id, model_name):
36+
self.id = id
37+
self._meta = MockMeta(model_name)
38+
self._state = MockState(adding=False)
39+
40+
# Test cases for each model caller
41+
model_names = [
42+
Device._meta.model_name,
43+
DeviceGroup._meta.model_name,
44+
Config._meta.model_name,
45+
]
46+
47+
for model_name in model_names:
48+
with self.subTest(model_name=model_name):
49+
mock_instance = MockInstance(id="test-id", model_name=model_name)
50+
kwargs = {}
51+
if model_name == Config._meta.model_name:
52+
kwargs["backend"] = "test-backend"
53+
kwargs["old_backend"] = "old-backend"
54+
elif model_name == DeviceGroup._meta.model_name:
55+
kwargs["templates"] = []
56+
kwargs["old_templates"] = []
57+
elif model_name == Device._meta.model_name:
58+
kwargs["group_id"] = "test-group"
59+
kwargs["old_group_id"] = "old-group"
60+
61+
# Case 1: Transaction commits
62+
mock_delay.reset_mock()
63+
with transaction.atomic():
64+
devicegroup_templates_change_handler(mock_instance, **kwargs)
65+
# Should not be called inside the transaction
66+
mock_delay.assert_not_called()
67+
68+
mock_delay.assert_called_once() # Should be called after commit
69+
70+
# Case 2: Transaction rolls back
71+
mock_delay.reset_mock()
72+
with self.assertRaises(ForceRollback):
73+
with transaction.atomic():
74+
devicegroup_templates_change_handler(mock_instance, **kwargs)
75+
mock_delay.assert_not_called()
76+
raise ForceRollback
77+
78+
# Should not be called because it rolled back
79+
mock_delay.assert_not_called()
80+
81+
@patch("openwisp_controller.config.tasks.change_devices_templates.delay")
82+
def test_devicegroup_templates_change_handler_list_branch(self, mock_delay):
83+
"""
84+
Test that the list branch of devicegroup_templates_change_handler
85+
(type(instance) is list) correctly dispatches change_devices_templates.delay
86+
after commit and not on rollback.
87+
"""
88+
instance_list = ["device-id-1", "device-id-2"]
89+
kwargs = {"group_id": "test-group", "old_group_id": "old-group"}
90+
91+
with self.subTest("list branch: delay called after commit"):
92+
mock_delay.reset_mock()
93+
with transaction.atomic():
94+
devicegroup_templates_change_handler(instance_list, **kwargs)
95+
# Should not be called inside the transaction
96+
mock_delay.assert_not_called()
97+
98+
mock_delay.assert_called_once() # Should be called after commit
99+
100+
with self.subTest("list branch: delay not called on rollback"):
101+
mock_delay.reset_mock()
102+
with self.assertRaises(ForceRollback):
103+
with transaction.atomic():
104+
devicegroup_templates_change_handler(instance_list, **kwargs)
105+
mock_delay.assert_not_called()
106+
raise ForceRollback
107+
108+
# Should not be called because it rolled back
109+
mock_delay.assert_not_called()

0 commit comments

Comments
 (0)