|
| 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 = self.env["queue.job"].with_delay( |
| 61 | + description="check default_env uid (superuser)" |
| 62 | + )._test_job() |
| 63 | + |
| 64 | + default_env_uid, record = self._capture_default_env_uid_during(delayable) |
| 65 | + |
| 66 | + self.assertEqual(record.state, "done") |
| 67 | + self.assertEqual(default_env_uid, SUPERUSER_ID) |
| 68 | + |
| 69 | + def test_runjob_without_updating_default_env_leaves_uid_none(self): |
| 70 | + """Repointing ``default_env`` is the specific mechanism this relies on. |
| 71 | +
|
| 72 | + Substitutes an alternative ``runjob`` that only creates a local |
| 73 | + superuser env (via ``http.request.env(user=SUPERUSER_ID)``) without |
| 74 | + touching ``transaction.default_env``, and verifies that the observable |
| 75 | + ``default_env.uid`` inside ``job.perform()`` is then ``None``. This |
| 76 | + isolates ``default_env`` repointing as the load-bearing behavior of |
| 77 | + ``runjob``: any future refactor that drops it will trip this test. |
| 78 | + """ |
| 79 | + |
| 80 | + def runjob_local_env_only(self_, db, job_uuid, **kw): |
| 81 | + """Alternative ``runjob`` that swaps only the local ``env``. |
| 82 | +
|
| 83 | + Uses ``http.request.env(user=SUPERUSER_ID)`` to obtain a |
| 84 | + superuser env for controller bookkeeping (acquire, lock, commit |
| 85 | + state changes), but does not update ``request.env`` or |
| 86 | + ``transaction.default_env``, which remain the ``uid=None`` env |
| 87 | + installed by ``_auth_method_none``. |
| 88 | + """ |
| 89 | + http.request.session.db = db |
| 90 | + env = http.request.env(user=SUPERUSER_ID) |
| 91 | + job = self_._acquire_job(env, job_uuid) |
| 92 | + if not job: |
| 93 | + return "" |
| 94 | + self_._runjob(env, job) |
| 95 | + return "" |
| 96 | + |
| 97 | + delayable = self.env["queue.job"].with_delay( |
| 98 | + description="check default_env uid (local env only)" |
| 99 | + )._test_job() |
| 100 | + |
| 101 | + with mock.patch.object(RunJobController, "runjob", runjob_local_env_only): |
| 102 | + default_env_uid, record = self._capture_default_env_uid_during(delayable) |
| 103 | + |
| 104 | + self.assertEqual(record.state, "done") |
| 105 | + self.assertIsNone(default_env_uid) |
0 commit comments