Skip to content

Commit 832c497

Browse files
test: add coverage for channel routing and identity keys in async dispatchers
1 parent c0af41c commit 832c497

2 files changed

Lines changed: 298 additions & 0 deletions

File tree

spp_programs/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@
3535
from . import test_bulk_membership
3636
from . import test_keyset_pagination
3737
from . import test_canary_patterns
38+
from . import test_concurrency
Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
# Part of OpenSPP. See LICENSE file for full copyright and licensing details.
2+
"""Tests for Phase 9: Job concurrency, channel routing, and identity keys.
3+
4+
Verify that async dispatchers pass correct channel and identity_key to
5+
delayable(), and that completion handlers route to statistics_refresh.
6+
"""
7+
8+
import uuid
9+
from unittest.mock import MagicMock, patch
10+
11+
from odoo import fields
12+
from odoo.tests import TransactionCase
13+
14+
15+
class TestCycleManagerChannelRouting(TransactionCase):
16+
"""Test channel routing and identity_key in cycle manager async methods."""
17+
18+
def setUp(self):
19+
super().setUp()
20+
self.program = self.env["spp.program"].create({"name": f"Test Program {uuid.uuid4().hex[:8]}"})
21+
self.cycle = self.env["spp.cycle"].create(
22+
{
23+
"name": "Test Cycle",
24+
"program_id": self.program.id,
25+
"start_date": fields.Date.today(),
26+
"end_date": fields.Date.today(),
27+
}
28+
)
29+
self.cycle_manager = self.env["spp.cycle.manager.default"].create(
30+
{
31+
"name": "Test Cycle Manager",
32+
"program_id": self.program.id,
33+
}
34+
)
35+
36+
def test_check_eligibility_async_uses_identity_key(self):
37+
"""_check_eligibility_async must pass identity_key to delayable."""
38+
partners = self.env["res.partner"].create(
39+
[{"name": f"Registrant {i}", "is_registrant": True} for i in range(3)]
40+
)
41+
self.env["spp.cycle.membership"].create(
42+
[{"partner_id": p.id, "cycle_id": self.cycle.id, "state": "draft"} for p in partners]
43+
)
44+
45+
delayable_calls = []
46+
original_delayable = type(self.cycle_manager).delayable
47+
48+
def mock_delayable(self_inner, **kwargs):
49+
delayable_calls.append(kwargs)
50+
return original_delayable(self_inner, **kwargs)
51+
52+
with patch.object(type(self.cycle_manager), "delayable", mock_delayable):
53+
try:
54+
self.cycle_manager._check_eligibility_async(self.cycle, 3)
55+
except Exception:
56+
pass
57+
58+
# Should have at least one call with identity_key containing "check_elig_"
59+
identity_keys = [c.get("identity_key", "") for c in delayable_calls]
60+
has_check_elig_key = any("check_elig_" in k for k in identity_keys)
61+
self.assertTrue(has_check_elig_key, f"Expected identity_key with 'check_elig_', got: {identity_keys}")
62+
63+
# Completion handler should route to statistics_refresh
64+
channels = [c.get("channel", "") for c in delayable_calls]
65+
self.assertIn("statistics_refresh", channels)
66+
67+
def test_prepare_entitlements_async_uses_identity_key(self):
68+
"""_prepare_entitlements_async must pass identity_key to delayable."""
69+
partners = self.env["res.partner"].create(
70+
[{"name": f"Registrant {i}", "is_registrant": True} for i in range(3)]
71+
)
72+
self.env["spp.cycle.membership"].create(
73+
[{"partner_id": p.id, "cycle_id": self.cycle.id, "state": "enrolled"} for p in partners]
74+
)
75+
76+
delayable_calls = []
77+
original_delayable = type(self.cycle_manager).delayable
78+
79+
def mock_delayable(self_inner, **kwargs):
80+
delayable_calls.append(kwargs)
81+
return original_delayable(self_inner, **kwargs)
82+
83+
with patch.object(type(self.cycle_manager), "delayable", mock_delayable):
84+
try:
85+
self.cycle_manager._prepare_entitlements_async(self.cycle, 3)
86+
except Exception:
87+
pass
88+
89+
identity_keys = [c.get("identity_key", "") for c in delayable_calls]
90+
has_prepare_key = any("prepare_ent_" in k for k in identity_keys)
91+
self.assertTrue(has_prepare_key, f"Expected identity_key with 'prepare_ent_', got: {identity_keys}")
92+
93+
channels = [c.get("channel", "") for c in delayable_calls]
94+
self.assertIn("statistics_refresh", channels)
95+
96+
def test_add_beneficiaries_async_uses_identity_key(self):
97+
"""_add_beneficiaries_async must pass identity_key to delayable."""
98+
partners = self.env["res.partner"].create(
99+
[{"name": f"Registrant {i}", "is_registrant": True} for i in range(3)]
100+
)
101+
102+
delayable_calls = []
103+
original_delayable = type(self.cycle_manager).delayable
104+
105+
def mock_delayable(self_inner, **kwargs):
106+
delayable_calls.append(kwargs)
107+
return original_delayable(self_inner, **kwargs)
108+
109+
with patch.object(type(self.cycle_manager), "delayable", mock_delayable):
110+
try:
111+
self.cycle_manager._add_beneficiaries_async(self.cycle, partners.ids, "draft")
112+
except Exception:
113+
pass
114+
115+
identity_keys = [c.get("identity_key", "") for c in delayable_calls]
116+
has_add_key = any("add_benef_" in k for k in identity_keys)
117+
self.assertTrue(has_add_key, f"Expected identity_key with 'add_benef_', got: {identity_keys}")
118+
119+
channels = [c.get("channel", "") for c in delayable_calls]
120+
self.assertIn("statistics_refresh", channels)
121+
122+
123+
class TestProgramManagerChannelRouting(TransactionCase):
124+
"""Test channel routing and identity_key in program manager async methods."""
125+
126+
def setUp(self):
127+
super().setUp()
128+
self.program = self.env["spp.program"].create({"name": f"Test Program {uuid.uuid4().hex[:8]}"})
129+
self.manager = self.env["spp.program.manager.default"].create(
130+
{
131+
"name": "Test Manager",
132+
"program_id": self.program.id,
133+
}
134+
)
135+
136+
def test_enroll_eligible_async_uses_identity_key(self):
137+
"""_enroll_eligible_registrants_async must pass identity_key to delayable."""
138+
partners = self.env["res.partner"].create(
139+
[{"name": f"Registrant {i}", "is_registrant": True} for i in range(3)]
140+
)
141+
self.env["spp.program.membership"].create(
142+
[{"partner_id": p.id, "program_id": self.program.id, "state": "draft"} for p in partners]
143+
)
144+
145+
delayable_calls = []
146+
original_delayable = type(self.manager).delayable
147+
148+
def mock_delayable(self_inner, **kwargs):
149+
delayable_calls.append(kwargs)
150+
return original_delayable(self_inner, **kwargs)
151+
152+
with patch.object(type(self.manager), "delayable", mock_delayable):
153+
try:
154+
self.manager._enroll_eligible_registrants_async(["draft"], 3)
155+
except Exception:
156+
pass
157+
158+
identity_keys = [c.get("identity_key", "") for c in delayable_calls]
159+
has_enroll_key = any("enroll_eligible_" in k for k in identity_keys)
160+
self.assertTrue(has_enroll_key, f"Expected identity_key with 'enroll_eligible_', got: {identity_keys}")
161+
162+
channels = [c.get("channel", "") for c in delayable_calls]
163+
self.assertIn("statistics_refresh", channels)
164+
165+
166+
class TestEligibilityManagerChannelRouting(TransactionCase):
167+
"""Test channel routing and identity_key in eligibility manager async methods."""
168+
169+
def setUp(self):
170+
super().setUp()
171+
self.program = self.env["spp.program"].create({"name": f"Test Program {uuid.uuid4().hex[:8]}"})
172+
self.elig_manager = self.env["spp.program.membership.manager.default"].create(
173+
{
174+
"name": "Test Elig Manager",
175+
"program_id": self.program.id,
176+
}
177+
)
178+
179+
def test_import_registrants_async_uses_identity_key(self):
180+
"""_import_registrants_async must pass identity_key to delayable."""
181+
partners = self.env["res.partner"].create(
182+
[{"name": f"Registrant {i}", "is_registrant": True} for i in range(3)]
183+
)
184+
185+
delayable_calls = []
186+
original_delayable = type(self.elig_manager).delayable
187+
188+
def mock_delayable(self_inner, **kwargs):
189+
delayable_calls.append(kwargs)
190+
return original_delayable(self_inner, **kwargs)
191+
192+
with patch.object(type(self.elig_manager), "delayable", mock_delayable):
193+
try:
194+
self.elig_manager._import_registrants_async(partners, "draft")
195+
except Exception:
196+
pass
197+
198+
identity_keys = [c.get("identity_key", "") for c in delayable_calls]
199+
has_import_key = any("import_reg_" in k for k in identity_keys)
200+
self.assertTrue(has_import_key, f"Expected identity_key with 'import_reg_', got: {identity_keys}")
201+
202+
channels = [c.get("channel", "") for c in delayable_calls]
203+
self.assertIn("statistics_refresh", channels)
204+
205+
206+
class TestEntitlementManagerChannelRouting(TransactionCase):
207+
"""Test that entitlement async methods route to entitlement_approval channel."""
208+
209+
def setUp(self):
210+
super().setUp()
211+
self.program = self.env["spp.program"].create({"name": f"Test Program {uuid.uuid4().hex[:8]}"})
212+
self.cycle = self.env["spp.cycle"].create(
213+
{
214+
"name": "Test Cycle",
215+
"program_id": self.program.id,
216+
"start_date": fields.Date.today(),
217+
"end_date": fields.Date.today(),
218+
}
219+
)
220+
221+
def _get_entitlement_manager(self):
222+
"""Get a cash entitlement manager for testing."""
223+
return self.env["spp.program.entitlement.manager.cash"].create(
224+
{
225+
"name": "Test Entitlement Manager",
226+
"program_id": self.program.id,
227+
}
228+
)
229+
230+
def test_set_pending_validation_async_routes_to_entitlement_approval(self):
231+
"""_set_pending_validation_entitlements_async must use entitlement_approval channel."""
232+
ent_manager = self._get_entitlement_manager()
233+
mock_entitlements = MagicMock()
234+
mock_entitlements.__len__ = MagicMock(return_value=5)
235+
mock_entitlements.__getitem__ = MagicMock(return_value=mock_entitlements)
236+
237+
delayable_calls = []
238+
original_delayable = type(ent_manager).delayable
239+
240+
def mock_delayable(self_inner, **kwargs):
241+
delayable_calls.append(kwargs)
242+
return original_delayable(self_inner, **kwargs)
243+
244+
with patch.object(type(ent_manager), "delayable", mock_delayable):
245+
try:
246+
ent_manager._set_pending_validation_entitlements_async(self.cycle, mock_entitlements)
247+
except Exception:
248+
pass
249+
250+
channels = [c.get("channel", "") for c in delayable_calls]
251+
self.assertIn("entitlement_approval", channels)
252+
253+
def test_validate_entitlements_async_routes_to_entitlement_approval(self):
254+
"""_validate_entitlements_async must use entitlement_approval channel."""
255+
ent_manager = self._get_entitlement_manager()
256+
mock_entitlements = MagicMock()
257+
mock_entitlements.__len__ = MagicMock(return_value=5)
258+
mock_entitlements.__getitem__ = MagicMock(return_value=mock_entitlements)
259+
260+
delayable_calls = []
261+
original_delayable = type(ent_manager).delayable
262+
263+
def mock_delayable(self_inner, **kwargs):
264+
delayable_calls.append(kwargs)
265+
return original_delayable(self_inner, **kwargs)
266+
267+
with patch.object(type(ent_manager), "delayable", mock_delayable):
268+
try:
269+
ent_manager._validate_entitlements_async(self.cycle, mock_entitlements, 5)
270+
except Exception:
271+
pass
272+
273+
channels = [c.get("channel", "") for c in delayable_calls]
274+
self.assertIn("entitlement_approval", channels)
275+
276+
def test_cancel_entitlements_async_routes_to_entitlement_approval(self):
277+
"""_cancel_entitlements_async must use entitlement_approval channel."""
278+
ent_manager = self._get_entitlement_manager()
279+
mock_entitlements = MagicMock()
280+
mock_entitlements.__len__ = MagicMock(return_value=5)
281+
mock_entitlements.__getitem__ = MagicMock(return_value=mock_entitlements)
282+
283+
delayable_calls = []
284+
original_delayable = type(ent_manager).delayable
285+
286+
def mock_delayable(self_inner, **kwargs):
287+
delayable_calls.append(kwargs)
288+
return original_delayable(self_inner, **kwargs)
289+
290+
with patch.object(type(ent_manager), "delayable", mock_delayable):
291+
try:
292+
ent_manager._cancel_entitlements_async(self.cycle, mock_entitlements, 5)
293+
except Exception:
294+
pass
295+
296+
channels = [c.get("channel", "") for c in delayable_calls]
297+
self.assertIn("entitlement_approval", channels)

0 commit comments

Comments
 (0)