From 415420f58256b35b1eaf7e3dd9a1d9782c3c55b8 Mon Sep 17 00:00:00 2001 From: Danny Adair Date: Wed, 22 Apr 2026 15:54:09 +1200 Subject: [PATCH 1/4] [FIX] queue_job: repoint default_env to SUPERUSER in runjob runjob is declared auth="none", so _auth_method_none pins request.env and transaction.default_env to a uid=None env. `env = http.request.env( user=SUPERUSER_ID)` only creates a local superuser env, leaving the default_env as uid=None. Any flush that goes through Transaction.flush() -> default_env.flush_all() then recomputes stored fields as uid=None and fails on anything dereferencing self.env.user. `http.request.update_env(user=SUPERUSER_ID)` additionally sets transaction.default_env to the superuser env. Closes #922 --- queue_job/controllers/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/queue_job/controllers/main.py b/queue_job/controllers/main.py index b4a5a596c..46da185d4 100644 --- a/queue_job/controllers/main.py +++ b/queue_job/controllers/main.py @@ -214,7 +214,8 @@ def _get_failure_values(cls, job, traceback_txt, orig_exception): readonly=False, ) def runjob(self, job_uuid, **kw): - env = http.request.env(user=SUPERUSER_ID) + http.request.update_env(user=SUPERUSER_ID) + env = http.request.env job = self._acquire_job(env, job_uuid) if not job: return "" From fd214cb0b07014b23d471b4da7b70e2a14e25b96 Mon Sep 17 00:00:00 2001 From: "Danny W. Adair" Date: Wed, 1 Jul 2026 11:13:47 +0000 Subject: [PATCH 2/4] [ADD] queue_job: HttpCase regression tests for /queue_job/runjob Adds test_run_job_controller_http.py covering #922: - test_runjob_pins_default_env_to_superuser asserts that when _acquire_job is entered, env.transaction.default_env.uid is SUPERUSER_ID -- the state a job would inherit at perform time. - test_runjob_without_updating_default_env_leaves_uid_none patches Request.update_env to a no-op and verifies default_env.uid stays None, isolating default_env repointing as the load-bearing behavior of runjob. Tests use HttpCase so _auth_method_none, session handling, and update_env run for real. _acquire_job is patched to return None so the endpoint exits cleanly without performing DB work (its internal commit is forbidden inside tests). --- queue_job/tests/__init__.py | 1 + .../tests/test_run_job_controller_http.py | 85 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 queue_job/tests/test_run_job_controller_http.py diff --git a/queue_job/tests/__init__.py b/queue_job/tests/__init__.py index 16bcdff96..696549a65 100644 --- a/queue_job/tests/__init__.py +++ b/queue_job/tests/__init__.py @@ -1,3 +1,4 @@ +from . import test_run_job_controller_http from . import test_run_rob_controller from . import test_runner_channels from . import test_runner_runner diff --git a/queue_job/tests/test_run_job_controller_http.py b/queue_job/tests/test_run_job_controller_http.py new file mode 100644 index 000000000..5ad88c1b8 --- /dev/null +++ b/queue_job/tests/test_run_job_controller_http.py @@ -0,0 +1,85 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest import mock + +from odoo import SUPERUSER_ID, http +from odoo.tests.common import HttpCase, tagged + +from ..controllers.main import RunJobController + + +@tagged("-at_install", "post_install") +class TestRunJobHttp(HttpCase): + """End-to-end tests for the ``/queue_job/runjob`` endpoint. + + ``runjob`` is declared ``auth="none"``, so ``_auth_method_none`` pins + ``transaction.default_env`` to an env with ``uid=None``. The controller + must additionally repoint ``default_env`` to a superuser env, otherwise + any flush during ``job.perform()`` that goes through + ``Transaction.flush() -> default_env.flush_all()`` recomputes stored + fields with ``self.env.user`` as an empty recordset. See #922. + """ + + def _capture_default_env_uid_at_acquire(self): + """Hit ``/queue_job/runjob`` and observe ``transaction.default_env.uid`` + at the moment ``_acquire_job`` is entered. + + ``_acquire_job`` is patched to return ``None`` so the endpoint exits + without performing the job. This avoids the commit inside + ``_acquire_job``, which is forbidden inside tests, and is sufficient + to observe the env state established by the request-handling + machinery: nothing between ``update_env`` and ``_acquire_job`` + touches ``transaction.default_env``, so the value seen here is the + same value a job would see at ``perform()`` time. + + HttpCase runs the HTTP server in the same process, so class-level + ``mock.patch`` calls take effect on the server side as well. + """ + captured = {} + + def spy(cls, env, job_uuid): + captured["default_env_uid"] = env.transaction.default_env.uid + return None + + with mock.patch.object(RunJobController, "_acquire_job", classmethod(spy)): + response = self.url_open( + f"/queue_job/runjob?db={self.env.cr.dbname}&job_uuid=stub" + ) + response.raise_for_status() + + return captured.get("default_env_uid") + + def test_runjob_pins_default_env_to_superuser(self): + """``runjob`` must set ``transaction.default_env`` to a superuser env. + + Verifies that by the time ``_acquire_job`` is entered, + ``env.transaction.default_env.uid`` is ``SUPERUSER_ID``. This is the + state that lets flushes triggered by the job recompute stored fields + with a real ``self.env.user`` rather than an empty recordset. + """ + default_env_uid = self._capture_default_env_uid_at_acquire() + self.assertEqual(default_env_uid, SUPERUSER_ID) + + def test_runjob_without_updating_default_env_leaves_uid_none(self): + """Repointing ``default_env`` is the specific mechanism this relies on. + + Neutralises ``http.request.update_env`` for the duration of the + request, matching what happens if a ``runjob`` implementation only + obtains a local superuser env via ``http.request.env(user=...)`` + without invoking ``update_env``. The observable ``default_env.uid`` + then remains at the ``None`` value installed by + ``_auth_method_none``. This isolates ``default_env`` repointing as + the load-bearing behavior of ``runjob``: any future refactor that + drops it will trip this test. + """ + + def no_op_update_env(self_, user=None, context=None, su=None): + """Alternative ``Request.update_env`` that intentionally does + nothing. Simulates a controller that never asks the request to + update its environment. + """ + + with mock.patch.object(http.Request, "update_env", no_op_update_env): + default_env_uid = self._capture_default_env_uid_at_acquire() + + self.assertIsNone(default_env_uid) From ffed8616d6169d32e889fed308acc7d9ddea2d81 Mon Sep 17 00:00:00 2001 From: Danny Adair Date: Thu, 16 Jul 2026 03:08:51 +1200 Subject: [PATCH 3/4] Adjust license from AGPL to LGPL Co-authored-by: Guewen Baconnier --- queue_job/tests/test_run_job_controller_http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/queue_job/tests/test_run_job_controller_http.py b/queue_job/tests/test_run_job_controller_http.py index 5ad88c1b8..6aaa30d01 100644 --- a/queue_job/tests/test_run_job_controller_http.py +++ b/queue_job/tests/test_run_job_controller_http.py @@ -1,4 +1,4 @@ -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). from unittest import mock From d2c1759102f1e0bc8f6244629b5b38c7b7882f36 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 20 Jul 2026 15:24:54 +0000 Subject: [PATCH 4/4] [BOT] post-merge updates --- README.md | 2 +- queue_job/README.rst | 2 +- queue_job/__manifest__.py | 2 +- queue_job/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7c5ddf577..76f29bd17 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Available addons ---------------- addon | version | maintainers | summary --- | --- | --- | --- -[queue_job](queue_job/) | 19.0.2.0.2 | guewen sbidoul | Job Queue +[queue_job](queue_job/) | 19.0.2.0.3 | guewen sbidoul | Job Queue [test_queue_job](test_queue_job/) | 19.0.2.0.0 | sbidoul | Queue Job Tests diff --git a/queue_job/README.rst b/queue_job/README.rst index 85162311a..35828a1b3 100644 --- a/queue_job/README.rst +++ b/queue_job/README.rst @@ -11,7 +11,7 @@ Job Queue !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:888b7dd2d71a49efa9e71a6e383ed5ababed3d72fa03ce75ffcc680dd8f1ba68 + !! source digest: sha256:3fa70ebecb05fe5e014778d029da704bcc083caf386201d7df82944dc3885c4d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png diff --git a/queue_job/__manifest__.py b/queue_job/__manifest__.py index 2cd3f8394..d071eba23 100644 --- a/queue_job/__manifest__.py +++ b/queue_job/__manifest__.py @@ -2,7 +2,7 @@ { "name": "Job Queue", - "version": "19.0.2.0.2", + "version": "19.0.2.0.3", "author": "Camptocamp,ACSONE SA/NV,Odoo Community Association (OCA)", "website": "https://github.com/OCA/queue", "license": "LGPL-3", diff --git a/queue_job/static/description/index.html b/queue_job/static/description/index.html index 24c6d315c..7cdc29df1 100644 --- a/queue_job/static/description/index.html +++ b/queue_job/static/description/index.html @@ -372,7 +372,7 @@

Job Queue

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:888b7dd2d71a49efa9e71a6e383ed5ababed3d72fa03ce75ffcc680dd8f1ba68 +!! source digest: sha256:3fa70ebecb05fe5e014778d029da704bcc083caf386201d7df82944dc3885c4d !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Mature License: LGPL-3 OCA/queue Translate me on Weblate Try me on Runboat

This addon adds an integrated Job Queue to Odoo.