From d3e80fa7fb2a3a3041244eb9a22acff157eaa63f Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Mon, 21 Sep 2026 13:57:59 +0800 Subject: [PATCH 1/3] test(spp_dci_client): assert outgoing-log integration at the log-service seam TestOutgoingLogIntegration skips without spp_api_v2, so it had never run before the full-stack CI. The client writes the audit row through a separate, committed cursor on purpose (the row must survive the request's rollback); a TransactionCase at REPEATABLE READ cannot see that row and its latest-row query found an earlier class's committed success entry instead, so every status assertion failed against a stale row. test_401_retry also built an OAuth2 source without the fields its own constraint requires. The tests now patch OutgoingApiLogService.log_call and assert the kwargs of each call; one test lets the real write happen and reads the row back through a fresh cursor, scoped by a per-test service code, and removes it; the 401 test builds a valid OAuth2 source, keeps the token flow out of the patched HTTP client, and asserts the two calls in the order the code guarantees. Refs #443 --- spp_dci_client/__manifest__.py | 2 +- spp_dci_client/readme/HISTORY.md | 4 + .../tests/test_outgoing_log_integration.py | 409 ++++++++---------- 3 files changed, 187 insertions(+), 228 deletions(-) diff --git a/spp_dci_client/__manifest__.py b/spp_dci_client/__manifest__.py index 457a40662..84f9afa46 100644 --- a/spp_dci_client/__manifest__.py +++ b/spp_dci_client/__manifest__.py @@ -2,7 +2,7 @@ { "name": "OpenSPP DCI Client", "summary": "Base DCI client infrastructure with OAuth2 and data source management", - "version": "19.0.2.0.1", + "version": "19.0.2.0.2", "category": "OpenSPP/Integration", "author": "OpenSPP.org", "website": "https://github.com/OpenSPP/OpenSPP2", diff --git a/spp_dci_client/readme/HISTORY.md b/spp_dci_client/readme/HISTORY.md index 4aaf9afef..184296f20 100644 --- a/spp_dci_client/readme/HISTORY.md +++ b/spp_dci_client/readme/HISTORY.md @@ -1,3 +1,7 @@ +### 19.0.2.0.2 + +- test(dci_client): make the outgoing-log integration tests hold on a database that has `spp_api_v2` installed, the only place they run at all (they skip without `spp.api.outgoing.log`, so they had never executed before the full-stack CI). The client writes the audit row through a separate, committed cursor so it survives the request's rollback; a test transaction at REPEATABLE READ cannot see that row and instead found an earlier class's committed row, so every status assertion compared against a stale success entry. The tests now assert at the log-service seam, one test reads a real row back through a fresh cursor and removes it, and the 401-retry test builds a valid OAuth2 source and asserts the two log calls in the order the code guarantees. No behaviour change (#443) + ### 19.0.2.0.0 - Initial migration to OpenSPP2 diff --git a/spp_dci_client/tests/test_outgoing_log_integration.py b/spp_dci_client/tests/test_outgoing_log_integration.py index 7cefec242..e7350d03c 100644 --- a/spp_dci_client/tests/test_outgoing_log_integration.py +++ b/spp_dci_client/tests/test_outgoing_log_integration.py @@ -2,6 +2,7 @@ """Tests for outgoing API log integration in DCI client""" import unittest +import uuid from unittest.mock import MagicMock, patch from odoo.tests import TransactionCase @@ -96,14 +97,21 @@ def test_copy_envelope_for_log_returns_copy(self): self.assertEqual(copied, envelope) -@unittest.skipUnless( - True, # Actual check is in setUpClass since we need the Odoo env - "spp_api_v2 required", -) class TestOutgoingLogIntegration(TransactionCase): """Test DCI client integration with outgoing API log. - These tests require spp_api_v2 to be installed (provides spp.api.outgoing.log). + These tests require spp_api_v2 to be installed (provides spp.api.outgoing.log), + so they run only on a database that has both modules — a full stack, never + spp_dci_client's own module CI. + + Assertions are made at the ``OutgoingApiLogService`` seam rather than by + reading rows back. ``_log_outgoing_call`` deliberately writes through a + separate, committed cursor so the audit row survives the request's + rollback; a plain ``TransactionCase`` runs at REPEATABLE READ and cannot + see rows committed after its snapshot, while it can see rows committed by + earlier test classes, so a "latest row" query returns another class's row. + One test still proves the real write, reading it back through a fresh + cursor of its own and cleaning up. """ @classmethod @@ -114,6 +122,18 @@ def setUpClass(cls): cls.DataSource = cls.env["spp.dci.data.source"] cls.OutgoingLog = cls.env["spp.api.outgoing.log"] + def setUp(self): + super().setUp() + from odoo.addons.spp_api_v2.services.outgoing_api_log_service import OutgoingApiLogService + + # The service is imported inside _log_outgoing_call from this module, so + # patching the class attribute is seen by production code. The cursor is + # still opened and the service still constructed for real; only the + # INSERT is replaced, so nothing is committed. + patcher = patch.object(OutgoingApiLogService, "log_call", autospec=True) + self.log_call = patcher.start() + self.addCleanup(patcher.stop) + def _create_test_data_source(self, **kwargs): """Helper to create a test data source""" vals = { @@ -137,274 +157,225 @@ def _make_mock_response(self, status_code=200, json_data=None): mock_response.raise_for_status = MagicMock() return mock_response + def _mock_http(self, mock_client_class, *, response=None, side_effect=None): + """Wire ``httpx.Client`` to one canned response or exception per call.""" + mock_http_client = MagicMock() + if side_effect is not None: + mock_http_client.post.side_effect = side_effect + else: + mock_http_client.post.return_value = response + mock_http_client.__enter__.return_value = mock_http_client + mock_http_client.__exit__.return_value = None + mock_client_class.return_value = mock_http_client + return mock_http_client + def _build_test_envelope(self, client): """Helper to build a test envelope""" return client._build_envelope(action="search", message={"test": "data"}) - def _find_latest_log(self, url_filter="crvs.example.org"): - """Helper to find the most recent outgoing log entry""" - return self.OutgoingLog.search( - [("url", "like", url_filter)], - order="id desc", - limit=1, - ) + def _logged(self): + """The kwargs of every log_call made during the test, in call order.""" + return [call.kwargs for call in self.log_call.call_args_list] + + def _single_logged(self): + logged = self._logged() + self.assertEqual(len(logged), 1, f"expected exactly one outgoing log call, got {len(logged)}") + return logged[0] + + def _request_expecting_user_error(self, client, envelope): + from odoo.exceptions import UserError + + with self.assertRaises(UserError): + client._make_request("/registry/sync/search", envelope) @patch("httpx.Client") def test_make_request_logs_success(self, mock_client_class): - """Successful request creates outgoing log with status=success""" + """Successful request logs status=success with the call's details.""" from ..services.client import DCIClient ds = self._create_test_data_source() client = DCIClient(ds, self.env) + self._mock_http(mock_client_class, response=self._make_mock_response()) - mock_response = self._make_mock_response() - mock_http_client = MagicMock() - mock_http_client.post.return_value = mock_response - mock_http_client.__enter__.return_value = mock_http_client - mock_http_client.__exit__.return_value = None - mock_client_class.return_value = mock_http_client + client._make_request("/registry/sync/search", self._build_test_envelope(client)) - envelope = self._build_test_envelope(client) - client._make_request("/registry/sync/search", envelope) + log = self._single_logged() + self.assertEqual(log["status"], "success") + self.assertEqual(log["response_status_code"], 200) + self.assertEqual(log["endpoint"], "/registry/sync/search") + self.assertEqual(log["url"], "https://crvs.example.org/api/registry/sync/search") + self.assertEqual(log["origin_model"], "spp.dci.data.source") + self.assertEqual(log["origin_record_id"], ds.id) + # A mocked HTTP round trip completes in well under a millisecond. + self.assertGreaterEqual(log["duration_ms"], 0) + self.assertIsNone(log["error_detail"]) - log = self._find_latest_log() - self.assertTrue(log, "Outgoing log should be created on success") - self.assertEqual(log.status, "success") - self.assertEqual(log.response_status_code, 200) - self.assertEqual(log.endpoint, "/registry/sync/search") - self.assertEqual(log.service_name, "DCI Client") - self.assertEqual(log.service_code, "test_crvs") - self.assertGreater(log.duration_ms, 0) + @patch("httpx.Client") + def test_service_is_built_for_the_data_source(self, mock_client_class): + """The log service carries the client's identity, not the model's default.""" + from odoo.addons.spp_api_v2.services.outgoing_api_log_service import OutgoingApiLogService + + from ..services.client import DCIClient + + ds = self._create_test_data_source() + client = DCIClient(ds, self.env) + self._mock_http(mock_client_class, response=self._make_mock_response()) + + with patch.object(OutgoingApiLogService, "__init__", return_value=None, autospec=True) as init: + client._make_request("/registry/sync/search", self._build_test_envelope(client)) + + self.assertEqual(init.call_count, 1) + kwargs = init.call_args.kwargs + self.assertEqual(kwargs["service_name"], "DCI Client") + self.assertEqual(kwargs["service_code"], "test_crvs") + self.assertEqual(kwargs["user_id"], self.env.uid) @patch("httpx.Client") def test_make_request_logs_http_error(self, mock_client_class): - """HTTP error creates outgoing log with status=http_error""" + """HTTP error logs status=http_error with the response code.""" import httpx from ..services.client import DCIClient ds = self._create_test_data_source() client = DCIClient(ds, self.env) - mock_response = MagicMock() mock_response.status_code = 500 mock_response.text = "Internal Server Error" mock_response.json.return_value = {"error": "server_error"} - mock_request = MagicMock() def raise_status(): - raise httpx.HTTPStatusError("Server Error", request=mock_request, response=mock_response) + raise httpx.HTTPStatusError("Server Error", request=MagicMock(), response=mock_response) mock_response.raise_for_status = raise_status + self._mock_http(mock_client_class, response=mock_response) - mock_http_client = MagicMock() - mock_http_client.post.return_value = mock_response - mock_http_client.__enter__.return_value = mock_http_client - mock_http_client.__exit__.return_value = None - mock_client_class.return_value = mock_http_client - - envelope = self._build_test_envelope(client) - - from odoo.exceptions import UserError - - with self.assertRaises(UserError): - client._make_request("/registry/sync/search", envelope) - - log = self._find_latest_log() - self.assertTrue(log, "Outgoing log should be created on HTTP error") - self.assertEqual(log.status, "http_error") - self.assertEqual(log.response_status_code, 500) - self.assertTrue(log.error_detail) + self._request_expecting_user_error(client, self._build_test_envelope(client)) - @patch("httpx.Client") - def test_make_request_logs_connection_error(self, mock_client_class): - """Connection error creates outgoing log with status=connection_error""" - import httpx + log = self._single_logged() + self.assertEqual(log["status"], "http_error") + self.assertEqual(log["response_status_code"], 500) + self.assertTrue(log["error_detail"]) + def _assert_exception_logged_as(self, mock_client_class, exception, status, detail_contains=None): from ..services.client import DCIClient ds = self._create_test_data_source() client = DCIClient(ds, self.env) + self._mock_http(mock_client_class, side_effect=exception) - mock_http_client = MagicMock() - mock_http_client.post.side_effect = httpx.ConnectError("Connection refused") - mock_http_client.__enter__.return_value = mock_http_client - mock_http_client.__exit__.return_value = None - mock_client_class.return_value = mock_http_client - - envelope = self._build_test_envelope(client) + self._request_expecting_user_error(client, self._build_test_envelope(client)) - from odoo.exceptions import UserError + log = self._single_logged() + self.assertEqual(log["status"], status) + self.assertTrue(log["error_detail"]) + if detail_contains: + self.assertIn(detail_contains, log["error_detail"].upper()) + self.assertIsNone(log["response_status_code"]) - with self.assertRaises(UserError): - client._make_request("/registry/sync/search", envelope) + @patch("httpx.Client") + def test_make_request_logs_connection_error(self, mock_client_class): + import httpx - log = self._find_latest_log() - self.assertTrue(log, "Outgoing log should be created on connection error") - self.assertEqual(log.status, "connection_error") - self.assertTrue(log.error_detail) + self._assert_exception_logged_as(mock_client_class, httpx.ConnectError("Connection refused"), "connection_error") @patch("httpx.Client") def test_make_request_logs_timeout(self, mock_client_class): - """Timeout creates outgoing log with status=timeout""" import httpx - from ..services.client import DCIClient - - ds = self._create_test_data_source() - client = DCIClient(ds, self.env) - - mock_http_client = MagicMock() - mock_http_client.post.side_effect = httpx.ReadTimeout("Read timed out") - mock_http_client.__enter__.return_value = mock_http_client - mock_http_client.__exit__.return_value = None - mock_client_class.return_value = mock_http_client - - envelope = self._build_test_envelope(client) - - from odoo.exceptions import UserError - - with self.assertRaises(UserError): - client._make_request("/registry/sync/search", envelope) - - log = self._find_latest_log() - self.assertTrue(log, "Outgoing log should be created on timeout") - self.assertEqual(log.status, "timeout") - self.assertTrue(log.error_detail) + self._assert_exception_logged_as(mock_client_class, httpx.ReadTimeout("Read timed out"), "timeout") @patch("httpx.Client") def test_make_request_logs_ssl_error(self, mock_client_class): - """SSL error creates outgoing log with status=connection_error""" import httpx - from ..services.client import DCIClient - - ds = self._create_test_data_source() - client = DCIClient(ds, self.env) - - mock_http_client = MagicMock() - mock_http_client.post.side_effect = httpx.ConnectError( - "[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed" + self._assert_exception_logged_as( + mock_client_class, + httpx.ConnectError("[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed"), + "connection_error", + detail_contains="SSL", ) - mock_http_client.__enter__.return_value = mock_http_client - mock_http_client.__exit__.return_value = None - mock_client_class.return_value = mock_http_client - - envelope = self._build_test_envelope(client) - - from odoo.exceptions import UserError - - with self.assertRaises(UserError): - client._make_request("/registry/sync/search", envelope) - - log = self._find_latest_log() - self.assertTrue(log, "Outgoing log should be created on SSL error") - self.assertEqual(log.status, "connection_error") - self.assertIn("SSL", log.error_detail.upper()) @patch("httpx.Client") def test_make_request_logs_dns_error(self, mock_client_class): - """DNS resolution error creates outgoing log with status=connection_error""" import httpx - from ..services.client import DCIClient - - ds = self._create_test_data_source() - client = DCIClient(ds, self.env) - - mock_http_client = MagicMock() - mock_http_client.post.side_effect = httpx.ConnectError("Name or service not known") - mock_http_client.__enter__.return_value = mock_http_client - mock_http_client.__exit__.return_value = None - mock_client_class.return_value = mock_http_client - - envelope = self._build_test_envelope(client) - - from odoo.exceptions import UserError - - with self.assertRaises(UserError): - client._make_request("/registry/sync/search", envelope) - - log = self._find_latest_log() - self.assertTrue(log, "Outgoing log should be created on DNS error") - self.assertEqual(log.status, "connection_error") - self.assertTrue(log.error_detail) + self._assert_exception_logged_as( + mock_client_class, httpx.ConnectError("Name or service not known"), "connection_error" + ) @patch("httpx.Client") def test_make_request_logs_generic_exception(self, mock_client_class): - """Generic exception creates outgoing log with status=error""" - from ..services.client import DCIClient - - ds = self._create_test_data_source() - client = DCIClient(ds, self.env) - - mock_http_client = MagicMock() - mock_http_client.post.side_effect = RuntimeError("Something unexpected") - mock_http_client.__enter__.return_value = mock_http_client - mock_http_client.__exit__.return_value = None - mock_client_class.return_value = mock_http_client - - envelope = self._build_test_envelope(client) - - from odoo.exceptions import UserError - - with self.assertRaises(UserError): - client._make_request("/registry/sync/search", envelope) - - log = self._find_latest_log() - self.assertTrue(log, "Outgoing log should be created on generic exception") - self.assertEqual(log.status, "error") - self.assertTrue(log.error_detail) + self._assert_exception_logged_as(mock_client_class, RuntimeError("Something unexpected"), "error") @patch("httpx.Client") def test_401_retry_creates_two_log_entries(self, mock_client_class): - """401 retry path creates log entry for both the 401 and the retry result""" + """The 401 retry path logs both the 401 and the retried result, in call order. + + The retried request runs inside the first one, so its ``finally`` logs + first (the success), and the outer call's ``finally`` logs second (the + 401). Call order is what the code guarantees; row ids are not asserted. + """ from ..services.client import DCIClient - ds = self._create_test_data_source(auth_type="oauth2") + ds = self._create_test_data_source( + auth_type="oauth2", + oauth2_token_url="https://auth.example.org/token", + oauth2_client_id="client123", + oauth2_client_secret="secret456", + ) client = DCIClient(ds, self.env) - - # First call returns 401, second returns 200 mock_401_response = MagicMock() mock_401_response.status_code = 401 mock_401_response.text = "Unauthorized" mock_401_response.json.return_value = {"error": "invalid_token"} mock_401_response.raise_for_status = MagicMock() + self._mock_http(mock_client_class, side_effect=[mock_401_response, self._make_mock_response()]) + + # get_headers() would fetch the OAuth2 token over the same patched HTTP + # client and consume the canned 401; the token flow is not under test. + with ( + patch.object(type(ds), "get_headers", return_value={"Content-Type": "application/json"}), + patch.object(type(ds), "clear_oauth2_token_cache") as clear_cache, + ): + client._make_request("/registry/sync/search", self._build_test_envelope(client)) + + clear_cache.assert_called_once() + logged = self._logged() + self.assertEqual(len(logged), 2, "one log call for the retried success, one for the 401") + retried, first = logged + self.assertEqual(retried["status"], "success") + self.assertEqual(retried["response_status_code"], 200) + self.assertEqual(first["status"], "http_error") + self.assertEqual(first["response_status_code"], 401) + self.assertIn("retrying", first["error_detail"].lower()) + # The 401 response body is captured for troubleshooting. + self.assertEqual(first["response_summary"], {"error": "invalid_token"}) - mock_200_response = self._make_mock_response() - - mock_http_client = MagicMock() - mock_http_client.post.side_effect = [mock_401_response, mock_200_response] - mock_http_client.__enter__.return_value = mock_http_client - mock_http_client.__exit__.return_value = None - mock_client_class.return_value = mock_http_client + @patch("httpx.Client") + def test_log_row_is_committed_on_a_separate_cursor(self, mock_client_class): + """The audit row really is written, on its own cursor, so it survives the + request's rollback. Read back through a fresh cursor (a new snapshot), + scoped by a per-test service code, and removed the same way.""" + from ..services.client import DCIClient - envelope = self._build_test_envelope(client) + # Let the real service write for this test only. + patch.stopall() + code = f"e2e_{uuid.uuid4().hex[:8]}" + ds = self._create_test_data_source(code=code, base_url=f"https://{code}.example.org/api") + client = DCIClient(ds, self.env) + self._mock_http(mock_client_class, response=self._make_mock_response()) - with patch.object(ds, "clear_oauth2_token_cache"): - client._make_request("/registry/sync/search", envelope) + client._make_request("/registry/sync/search", self._build_test_envelope(client)) - # Should have two log entries: one for 401, one for retry success - logs = self.OutgoingLog.search( - [("url", "like", "crvs.example.org")], - order="id desc", - limit=2, - ) - self.assertEqual(len(logs), 2, "Should have two log entries (401 + retry)") - - # With order="id desc", logs[0] has the highest ID (created last). - # Due to try-finally semantics, the recursive retry's finally runs first - # (lower ID = success), then the outer call's finally runs (higher ID = 401). - # So logs[0] (highest ID) is the 401 entry, logs[1] (lower ID) is the success. - self.assertEqual(logs[0].status, "http_error") - self.assertEqual(logs[0].response_status_code, 401) - self.assertIn("retrying", logs[0].error_detail.lower()) - # M-1 fix: 401 response body should be captured - self.assertTrue(logs[0].response_summary) - - # Earlier log (lower ID) is the retry success - self.assertEqual(logs[1].status, "success") - self.assertEqual(logs[1].response_status_code, 200) + with self.env.registry.cursor() as cr: + rows = self.env(cr=cr)["spp.api.outgoing.log"].search([("service_code", "=", code)]) + try: + self.assertEqual(len(rows), 1) + self.assertEqual(rows.status, "success") + self.assertEqual(rows.response_status_code, 200) + finally: + rows.unlink() @patch("httpx.Client") def test_log_failure_does_not_block_request(self, mock_client_class): @@ -413,33 +384,17 @@ def test_log_failure_does_not_block_request(self, mock_client_class): ds = self._create_test_data_source() client = DCIClient(ds, self.env) - - # Mock HTTP response (success) - mock_response = self._make_mock_response() - mock_http_client = MagicMock() - mock_http_client.post.return_value = mock_response - mock_http_client.__enter__.return_value = mock_http_client - mock_http_client.__exit__.return_value = None - mock_client_class.return_value = mock_http_client - - # Monkey-patch the model to raise - original_log_call = self.OutgoingLog.__class__.log_call - - def broken_log_call(self_model, **kwargs): - raise RuntimeError("Database error") - - self.OutgoingLog.__class__.log_call = broken_log_call - try: - # This should not raise despite broken logging - client._log_outgoing_call( - url="https://example.org/test", - endpoint="/test", - envelope={"header": {"action": "search"}, "message": {}}, - response_data=None, - status_code=200, - duration_ms=100, - status="success", - error_detail=None, - ) - finally: - self.OutgoingLog.__class__.log_call = original_log_call + self._mock_http(mock_client_class, response=self._make_mock_response()) + self.log_call.side_effect = RuntimeError("Database error") + + # This should not raise despite broken logging + client._log_outgoing_call( + url="https://example.org/test", + endpoint="/test", + envelope={"header": {"action": "search"}, "message": {}}, + response_data=None, + status_code=200, + duration_ms=100, + status="success", + error_detail=None, + ) From 3ae03f863cbc889fb381acd41c77a5ab71c5c49f Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Mon, 21 Sep 2026 13:58:25 +0800 Subject: [PATCH 2/3] style(spp_dci_client): apply ruff-format to the outgoing-log tests --- spp_dci_client/tests/test_outgoing_log_integration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spp_dci_client/tests/test_outgoing_log_integration.py b/spp_dci_client/tests/test_outgoing_log_integration.py index e7350d03c..e0041453f 100644 --- a/spp_dci_client/tests/test_outgoing_log_integration.py +++ b/spp_dci_client/tests/test_outgoing_log_integration.py @@ -277,7 +277,9 @@ def _assert_exception_logged_as(self, mock_client_class, exception, status, deta def test_make_request_logs_connection_error(self, mock_client_class): import httpx - self._assert_exception_logged_as(mock_client_class, httpx.ConnectError("Connection refused"), "connection_error") + self._assert_exception_logged_as( + mock_client_class, httpx.ConnectError("Connection refused"), "connection_error" + ) @patch("httpx.Client") def test_make_request_logs_timeout(self, mock_client_class): From 262d45d115a1169f6f6d024225b2871ca5691b9a Mon Sep 17 00:00:00 2001 From: Edwin Gonzales Date: Mon, 21 Sep 2026 14:12:23 +0800 Subject: [PATCH 3/3] docs(spp_dci_client): regenerate README from fragments (CI output) --- spp_dci_client/README.rst | 16 ++++++++++++++++ spp_dci_client/static/description/index.html | 17 +++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/spp_dci_client/README.rst b/spp_dci_client/README.rst index e3ecc9a56..e36bd4c04 100644 --- a/spp_dci_client/README.rst +++ b/spp_dci_client/README.rst @@ -140,6 +140,22 @@ Dependencies Changelog ========= +19.0.2.0.2 +~~~~~~~~~~ + +- test(dci_client): make the outgoing-log integration tests hold on a + database that has ``spp_api_v2`` installed, the only place they run at + all (they skip without ``spp.api.outgoing.log``, so they had never + executed before the full-stack CI). The client writes the audit row + through a separate, committed cursor so it survives the request's + rollback; a test transaction at REPEATABLE READ cannot see that row + and instead found an earlier class's committed row, so every status + assertion compared against a stale success entry. The tests now assert + at the log-service seam, one test reads a real row back through a + fresh cursor and removes it, and the 401-retry test builds a valid + OAuth2 source and asserts the two log calls in the order the code + guarantees. No behaviour change (#443) + 19.0.2.0.0 ~~~~~~~~~~ diff --git a/spp_dci_client/static/description/index.html b/spp_dci_client/static/description/index.html index 11e9ca70c..6b689ed00 100644 --- a/spp_dci_client/static/description/index.html +++ b/spp_dci_client/static/description/index.html @@ -514,6 +514,23 @@

Changelog

+

19.0.2.0.2

+
    +
  • test(dci_client): make the outgoing-log integration tests hold on a +database that has spp_api_v2 installed, the only place they run at +all (they skip without spp.api.outgoing.log, so they had never +executed before the full-stack CI). The client writes the audit row +through a separate, committed cursor so it survives the request’s +rollback; a test transaction at REPEATABLE READ cannot see that row +and instead found an earlier class’s committed row, so every status +assertion compared against a stale success entry. The tests now assert +at the log-service seam, one test reads a real row back through a +fresh cursor and removes it, and the 401-retry test builds a valid +OAuth2 source and asserts the two log calls in the order the code +guarantees. No behaviour change (#443)
  • +
+
+

19.0.2.0.0

  • Initial migration to OpenSPP2