feat(poll): make exact transitions idempotent
This commit is contained in:
@@ -266,12 +266,86 @@ class PollLifecycleTests(unittest.TestCase):
|
||||
idempotency_key="decision-2",
|
||||
)
|
||||
|
||||
def test_repeated_exact_transition_without_an_identity_is_rejected(self) -> None:
|
||||
def test_repeated_exact_transition_without_an_identity_is_an_audit_free_noop(self) -> None:
|
||||
poll = self._poll()
|
||||
applied = transition_poll(self.session, tenant_id="tenant-1", poll_id=poll.id, action="open")
|
||||
|
||||
with self.assertLogs("govoplan.poll.lifecycle", level="INFO") as logs:
|
||||
repeated = transition_poll(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
poll_id=poll.id,
|
||||
action="open",
|
||||
actor_user_id="owner",
|
||||
)
|
||||
|
||||
self.assertFalse(applied.replayed)
|
||||
self.assertTrue(repeated.replayed)
|
||||
self.assertIsNone(repeated.transition)
|
||||
self.assertEqual(poll.status, "open")
|
||||
self.assertEqual(len(poll.lifecycle_transitions), 1)
|
||||
self.assertIn("Ignored repeated exact Poll lifecycle transition", logs.output[0])
|
||||
|
||||
def test_same_decision_is_a_noop_but_a_changed_decision_is_audited(self) -> None:
|
||||
poll = self._poll()
|
||||
transition_poll(self.session, tenant_id="tenant-1", poll_id=poll.id, action="open")
|
||||
transition_poll(self.session, tenant_id="tenant-1", poll_id=poll.id, action="close")
|
||||
first = transition_poll(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
poll_id=poll.id,
|
||||
action="decide",
|
||||
option_key="yes",
|
||||
)
|
||||
repeated = transition_poll(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
poll_id=poll.id,
|
||||
action="decide",
|
||||
option_key="yes",
|
||||
)
|
||||
changed = transition_poll(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
poll_id=poll.id,
|
||||
action="decide",
|
||||
option_key="no",
|
||||
)
|
||||
|
||||
self.assertTrue(repeated.replayed)
|
||||
self.assertIsNone(repeated.transition)
|
||||
self.assertIsNotNone(first.transition)
|
||||
self.assertIsNotNone(changed.transition)
|
||||
self.assertEqual(changed.transition.previous_decision_option_id, first.transition.decision_option_id)
|
||||
self.assertEqual(
|
||||
[item.action for item in poll.lifecycle_transitions],
|
||||
["open", "close", "decide", "decide"],
|
||||
)
|
||||
|
||||
def test_exact_unarchive_retry_is_a_noop_but_unarchive_without_history_is_invalid(self) -> None:
|
||||
poll = self._poll()
|
||||
transition_poll(self.session, tenant_id="tenant-1", poll_id=poll.id, action="archive")
|
||||
transition_poll(self.session, tenant_id="tenant-1", poll_id=poll.id, action="unarchive")
|
||||
|
||||
repeated = transition_poll(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
poll_id=poll.id,
|
||||
action="unarchive",
|
||||
)
|
||||
|
||||
self.assertTrue(repeated.replayed)
|
||||
self.assertIsNone(repeated.transition)
|
||||
self.assertEqual([item.action for item in poll.lifecycle_transitions], ["archive", "unarchive"])
|
||||
|
||||
never_archived = self._poll(title="Never archived")
|
||||
with self.assertRaisesRegex(PollError, "not allowed"):
|
||||
transition_poll(self.session, tenant_id="tenant-1", poll_id=poll.id, action="open")
|
||||
transition_poll(
|
||||
self.session,
|
||||
tenant_id="tenant-1",
|
||||
poll_id=never_archived.id,
|
||||
action="unarchive",
|
||||
)
|
||||
|
||||
def test_archive_and_unarchive_restore_status_and_append_audit_history(self) -> None:
|
||||
poll = self._poll()
|
||||
@@ -444,6 +518,7 @@ class PollLifecycleTests(unittest.TestCase):
|
||||
self.assertEqual(opened.poll.status, "open")
|
||||
self.assertFalse(opened.replayed)
|
||||
self.assertTrue(replay.replayed)
|
||||
self.assertIsNotNone(replay.transition)
|
||||
self.assertEqual(len(lifecycle.history), 1)
|
||||
self.assertEqual(lifecycle.history[0].actor_user_id, "owner-membership")
|
||||
self.assertEqual(
|
||||
@@ -462,6 +537,27 @@ class PollLifecycleTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(rejected.exception.status_code, 400)
|
||||
|
||||
def test_generic_api_returns_null_transition_for_domain_noop(self) -> None:
|
||||
poll = self._poll(status="open", title="Already open")
|
||||
|
||||
repeated = api_transition_poll(
|
||||
poll.id,
|
||||
PollTransitionRequest(action="open"),
|
||||
idempotency_key=None,
|
||||
session=self.session,
|
||||
principal=self._principal(),
|
||||
)
|
||||
|
||||
self.assertEqual(repeated.poll.status, "open")
|
||||
self.assertTrue(repeated.replayed)
|
||||
self.assertIsNone(repeated.transition)
|
||||
self.assertEqual(
|
||||
self.session.query(PollLifecycleTransition)
|
||||
.filter(PollLifecycleTransition.poll_id == poll.id)
|
||||
.count(),
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user