Skip to content

Commit eeea3ee

Browse files
committed
[ADD] queue_job: HttpCase regression tests for /queue_job/runjob
Adds an HttpJobCaseMixin in tests/common.py providing a reusable run_http_job() helper for tests that exercise /queue_job/runjob. New test_run_job_controller_http.py covers #922: - test_runjob_pins_default_env_to_superuser asserts that during job.perform() the transaction's default_env.uid is SUPERUSER_ID. - test_runjob_without_updating_default_env_leaves_uid_none substitutes an alternative runjob that only swaps the local env and verifies default_env.uid stays None, isolating default_env repointing as the load-bearing behavior of the endpoint.
1 parent 415420f commit eeea3ee

3 files changed

Lines changed: 148 additions & 0 deletions

File tree

queue_job/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from . import test_run_job_controller_http
12
from . import test_run_rob_controller
23
from . import test_runner_channels
34
from . import test_runner_runner

queue_job/tests/common.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,44 @@ def __repr__(self):
321321
return repr(self.calls)
322322

323323

324+
class HttpJobCaseMixin:
325+
"""Mixin for tests that exercise the ``/queue_job/runjob`` HTTP endpoint.
326+
327+
Combine with :class:`odoo.tests.common.HttpCase`. Example::
328+
329+
@tagged("-at_install", "post_install")
330+
class TestSomeJob(HttpJobCaseMixin, HttpCase):
331+
def test_something(self):
332+
delayable = self.env["queue.job"].with_delay()._test_job()
333+
record = self.run_http_job(delayable)
334+
self.assertEqual(record.state, "done")
335+
"""
336+
337+
def run_http_job(self, job_or_uuid):
338+
"""Hit ``/queue_job/runjob`` for the given job.
339+
340+
Commits the current transaction so the HTTP server's cursor can see
341+
the enqueued job, performs the request, then commits again to pull
342+
in state changes written by the request. Returns the reloaded
343+
``queue.job`` record.
344+
"""
345+
uuid = (
346+
job_or_uuid
347+
if isinstance(job_or_uuid, str)
348+
else job_or_uuid.db_record().uuid
349+
)
350+
# Commits are required so the HTTP server's cursor (running in a
351+
# different thread within the same process) can see the enqueued job
352+
# and then the test can observe the state changes it wrote back.
353+
self.env.cr.commit() # pylint: disable=invalid-commit
354+
response = self.url_open(
355+
f"/queue_job/runjob?db={self.env.cr.dbname}&job_uuid={uuid}"
356+
)
357+
response.raise_for_status()
358+
self.env.cr.commit() # pylint: disable=invalid-commit
359+
return self.env["queue.job"].search([("uuid", "=", uuid)])
360+
361+
324362
class JobCounter:
325363
def __init__(self, env):
326364
super().__init__()
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
2+
3+
from unittest import mock
4+
5+
from odoo import SUPERUSER_ID, http
6+
from odoo.tests.common import HttpCase, tagged
7+
8+
from ..controllers.main import RunJobController
9+
from ..job import Job
10+
from .common import HttpJobCaseMixin
11+
12+
13+
@tagged("-at_install", "post_install")
14+
class TestRunJobHttp(HttpJobCaseMixin, HttpCase):
15+
"""End-to-end tests for the ``/queue_job/runjob`` endpoint.
16+
17+
``runjob`` is declared ``auth="none"``, so ``_auth_method_none`` pins
18+
``transaction.default_env`` to an env with ``uid=None``. The controller
19+
must additionally repoint ``default_env`` to a superuser env, otherwise
20+
any flush during ``job.perform()`` that goes through
21+
``Transaction.flush() -> default_env.flush_all()`` recomputes stored
22+
fields with ``self.env.user`` as an empty recordset. See #922.
23+
"""
24+
25+
def _capture_default_env_uid_during(self, delayable):
26+
"""Run ``delayable`` via /queue_job/runjob and observe the transaction's
27+
``default_env.uid`` from inside ``job.perform()``.
28+
29+
Installs a spy around ``Job.perform`` that records
30+
``job.env.transaction.default_env.uid`` before delegating to the real
31+
implementation. HttpCase runs the HTTP server in the same process,
32+
so the class-level ``mock.patch`` takes effect on the server side too.
33+
34+
:param delayable: a delayed job (return value of
35+
``with_delay().method()``)
36+
:returns: ``(default_env_uid, queue_job_record)`` -- the uid observed
37+
during ``perform()`` and the reloaded ``queue.job`` record after
38+
the request completes.
39+
"""
40+
captured = {}
41+
original_perform = Job.perform
42+
43+
def spy(job):
44+
captured["default_env_uid"] = job.env.transaction.default_env.uid
45+
return original_perform(job)
46+
47+
with mock.patch.object(Job, "perform", spy):
48+
record = self.run_http_job(delayable)
49+
50+
return captured["default_env_uid"], record
51+
52+
def test_runjob_pins_default_env_to_superuser(self):
53+
"""``runjob`` must set ``transaction.default_env`` to a superuser env.
54+
55+
Verifies that during ``job.perform()``,
56+
``env.transaction.default_env.uid`` is ``SUPERUSER_ID``. This is the
57+
state that lets flushes triggered by the job recompute stored fields
58+
with a real ``self.env.user`` rather than an empty recordset.
59+
"""
60+
delayable = (
61+
self.env["queue.job"]
62+
.with_delay(description="check default_env uid (superuser)")
63+
._test_job()
64+
)
65+
66+
default_env_uid, record = self._capture_default_env_uid_during(delayable)
67+
68+
self.assertEqual(record.state, "done")
69+
self.assertEqual(default_env_uid, SUPERUSER_ID)
70+
71+
def test_runjob_without_updating_default_env_leaves_uid_none(self):
72+
"""Repointing ``default_env`` is the specific mechanism this relies on.
73+
74+
Substitutes an alternative ``runjob`` that only creates a local
75+
superuser env (via ``http.request.env(user=SUPERUSER_ID)``) without
76+
touching ``transaction.default_env``, and verifies that the observable
77+
``default_env.uid`` inside ``job.perform()`` is then ``None``. This
78+
isolates ``default_env`` repointing as the load-bearing behavior of
79+
``runjob``: any future refactor that drops it will trip this test.
80+
"""
81+
82+
def runjob_local_env_only(self_, db, job_uuid, **kw):
83+
"""Alternative ``runjob`` that swaps only the local ``env``.
84+
85+
Uses ``http.request.env(user=SUPERUSER_ID)`` to obtain a
86+
superuser env for controller bookkeeping (acquire, lock, commit
87+
state changes), but does not update ``request.env`` or
88+
``transaction.default_env``, which remain the ``uid=None`` env
89+
installed by ``_auth_method_none``.
90+
"""
91+
http.request.session.db = db
92+
env = http.request.env(user=SUPERUSER_ID)
93+
job = self_._acquire_job(env, job_uuid)
94+
if not job:
95+
return ""
96+
self_._runjob(env, job)
97+
return ""
98+
99+
delayable = (
100+
self.env["queue.job"]
101+
.with_delay(description="check default_env uid (local env only)")
102+
._test_job()
103+
)
104+
105+
with mock.patch.object(RunJobController, "runjob", runjob_local_env_only):
106+
default_env_uid, record = self._capture_default_env_uid_during(delayable)
107+
108+
self.assertEqual(record.state, "done")
109+
self.assertIsNone(default_env_uid)

0 commit comments

Comments
 (0)