fix(fusion_helpdesk_central): close magic-link race + cron savepoint + avg pivot
Findings from the post-feature code review on commit 396170b4. Addresses
the two CRITICAL + one HIGH + two MEDIUM issues; rest are deferred.
CRITICAL #1 — magic-link token race:
Two near-simultaneous POSTs on the same /engagement/<token>/approve
could both SELECT state='pending' under READ COMMITTED, both post
chatter, and let the last writer flip the outcome. Now the POST path
does an atomic UPDATE helpdesk_ticket SET token=NULL WHERE token=%s
AND state='pending' RETURNING id — the loser gets no row back and
renders the friendly invalid-link page. Verified live: 2 concurrent
POSTs → 1 wins, 1 loses, exactly 1 chatter row.
CRITICAL #2 — reminder cron without per-row savepoint:
Per CLAUDE.md rule #14, a DB failure mid-loop aborts the whole
transaction and silently kills the rest of the batch. Wrap each row's
send_mail+write in `with self.env.cr.savepoint()`. Also corrected the
success-count log (was len(stale), now actual sent count).
HIGH #3 — turnaround pivot summed instead of averaged:
fields.Float defaults to SUM aggregator; meaningless for per-ticket
decision delays. Added aggregator='avg' so the pivot reads "avg
turnaround per ticket" not "summed wait time".
HIGH #4 — added test_concurrent_claim_only_one_wins regression test
that fires two real HTTP POSTs against the same token and asserts
exactly one wins + exactly one approval chatter row exists.
MEDIUM #6 — cron nextcall pinned to 09:00 tomorrow so reminders land
in business hours regardless of when the module was last upgraded.
MEDIUM #10 — escalate failed owner-partner-create from WARNING to
ERROR (via _logger.exception) since silent attribution to the bot
account is a real audit-trail confusion.
Deferred (follow-up commits): #5, #7 (executor cleanup), #8, #9,
#11–#14 — none are bugs, all spec-drift or hardening.
This commit is contained in:
@@ -391,6 +391,56 @@ class TestEngagementPortal(HttpCase):
|
||||
t.unlink()
|
||||
self.env.cr.commit()
|
||||
|
||||
def test_concurrent_claim_only_one_wins(self):
|
||||
"""Regression for the magic-link double-click race.
|
||||
|
||||
Two POSTs against the same token must NOT both record decisions.
|
||||
The controller uses UPDATE...RETURNING with a WHERE on
|
||||
state='pending' so the second call gets a NULL row back and
|
||||
returns the invalid-link page. Without that atomic claim, two
|
||||
worker transactions could each SELECT the same pending row and
|
||||
both post chatter — last-writer-wins on state.
|
||||
|
||||
url_open hits live HTTP, so each call is its own request/
|
||||
transaction — different from a same-transaction simulation and
|
||||
the actual production race scenario.
|
||||
"""
|
||||
t = self._make_pending_ticket()
|
||||
token = t.x_fc_engagement_token
|
||||
try:
|
||||
r1 = self.url_open(
|
||||
'/fusion_helpdesk/engagement/%s/approve' % token,
|
||||
data={'comment': 'first'}, timeout=10,
|
||||
)
|
||||
r2 = self.url_open(
|
||||
'/fusion_helpdesk/engagement/%s/approve' % token,
|
||||
data={'comment': 'second'}, timeout=10,
|
||||
)
|
||||
self.assertEqual(r1.status_code, 200)
|
||||
self.assertEqual(r2.status_code, 200)
|
||||
ok_count = sum(
|
||||
'Approval recorded' in r.text for r in (r1, r2))
|
||||
invalid_count = sum(
|
||||
'Link no longer valid' in r.text for r in (r1, r2))
|
||||
self.assertEqual(
|
||||
ok_count, 1,
|
||||
'Both clicks must not both succeed (race condition).',
|
||||
)
|
||||
self.assertEqual(invalid_count, 1)
|
||||
t.invalidate_recordset()
|
||||
approval_chatter = self.env['mail.message'].search_count([
|
||||
('res_id', '=', t.id),
|
||||
('model', '=', 'helpdesk.ticket'),
|
||||
('body', 'ilike', 'Approved by'),
|
||||
])
|
||||
self.assertEqual(
|
||||
approval_chatter, 1,
|
||||
'Race must not produce duplicate approval chatter posts.',
|
||||
)
|
||||
finally:
|
||||
t.unlink()
|
||||
self.env.cr.commit()
|
||||
|
||||
def test_post_records_decision_and_invalidates_token(self):
|
||||
t = self._make_pending_ticket()
|
||||
token = t.x_fc_engagement_token
|
||||
|
||||
Reference in New Issue
Block a user