diff --git a/webservice/data/neutralize.sql b/webservice/data/neutralize.sql new file mode 100644 index 00000000..441b3b4a --- /dev/null +++ b/webservice/data/neutralize.sql @@ -0,0 +1,7 @@ +-- Drop the OAuth2 secrets of the webservice backends, so that a database +-- copied from another environment does not keep the production credentials. +UPDATE webservice_backend + SET oauth2_clientid = NULL, + oauth2_client_secret = NULL, + oauth2_client_auth_value = NULL, + oauth2_token = NULL; diff --git a/webservice/tests/__init__.py b/webservice/tests/__init__.py index 37c85ace..90606345 100644 --- a/webservice/tests/__init__.py +++ b/webservice/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_content_only_compat +from . import test_neutralize from . import test_oauth2 diff --git a/webservice/tests/test_neutralize.py b/webservice/tests/test_neutralize.py new file mode 100644 index 00000000..e40d42b7 --- /dev/null +++ b/webservice/tests/test_neutralize.py @@ -0,0 +1,41 @@ +# Copyright 2026 Camptocamp SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.modules import neutralize +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestNeutralize(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.backend = cls.env["webservice.backend"].create( + { + "name": "Neutralize OAuth2", + "tech_name": "neutralize_oauth2", + "protocol": "http", + "url": "https://production.example.com/api", + "oauth2_clientid": "prod_client_id", + "oauth2_client_secret": "prod_client_secret", + "oauth2_token": "prod_token", + } + ) + + def _run_neutralize(self): + """Run the neutralization queries of the webservice module.""" + for query in neutralize.get_neutralization_queries(["webservice"]): + self.env.cr.execute(query) + self.backend.invalidate_recordset() + + def test_neutralize_clears_oauth2_secrets(self): + """OAuth2 client ID, client secret and token are cleared.""" + self._run_neutralize() + self.assertFalse(self.backend.oauth2_clientid) + self.assertFalse(self.backend.oauth2_client_secret) + self.assertFalse(self.backend.oauth2_token) + + def test_neutralize_keeps_endpoint(self): + """The endpoint is not touched by the OAuth2 neutralization.""" + self._run_neutralize() + self.assertEqual(self.backend.url, "https://production.example.com/api") diff --git a/webservice_core/data/neutralize.sql b/webservice_core/data/neutralize.sql new file mode 100644 index 00000000..460e40da --- /dev/null +++ b/webservice_core/data/neutralize.sql @@ -0,0 +1,8 @@ +-- Drop the endpoints and credentials of the webservice backends, so that +-- a database copied from another environment does not call external services. +-- `url` is required, so it gets a placeholder instead of NULL. +UPDATE webservice_backend + SET url = 'https://neutralized.invalid', + username = NULL, + password = NULL, + api_key = NULL; diff --git a/webservice_core/tests/__init__.py b/webservice_core/tests/__init__.py index 29905be6..dfb7f117 100644 --- a/webservice_core/tests/__init__.py +++ b/webservice_core/tests/__init__.py @@ -1,3 +1,4 @@ +from . import test_neutralize from . import test_utils from . import test_webservice from . import test_webservice_endpoint diff --git a/webservice_core/tests/test_neutralize.py b/webservice_core/tests/test_neutralize.py new file mode 100644 index 00000000..49ec3fe0 --- /dev/null +++ b/webservice_core/tests/test_neutralize.py @@ -0,0 +1,47 @@ +# Copyright 2026 Camptocamp SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo.modules import neutralize +from odoo.tests.common import TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestNeutralize(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.backend = cls.env["webservice.backend"].create( + { + "name": "Neutralize me", + "tech_name": "neutralize_me", + "protocol": "http", + "url": "https://production.example.com/api", + "username": "prod_user", + "password": "prod_password", + "api_key": "prod_api_key", + } + ) + + def _run_neutralize(self): + """Run the neutralization queries of the webservice_core module.""" + for query in neutralize.get_neutralization_queries(["webservice_core"]): + self.env.cr.execute(query) + self.backend.invalidate_recordset() + + def test_neutralize_replaces_url(self): + """The production URL is replaced by a placeholder.""" + self._run_neutralize() + self.assertEqual(self.backend.url, "https://neutralized.invalid") + + def test_neutralize_clears_credentials(self): + """Username, password and API key of the backend are cleared.""" + self._run_neutralize() + self.assertFalse(self.backend.username) + self.assertFalse(self.backend.password) + self.assertFalse(self.backend.api_key) + + def test_neutralize_keeps_other_fields(self): + """Fields that are not endpoints or credentials are left untouched.""" + self._run_neutralize() + self.assertEqual(self.backend.name, "Neutralize me") + self.assertEqual(self.backend.protocol, "http")