diff --git a/server/app/interfaces/base.py b/server/app/interfaces/base.py index ac974cba..927dfafc 100644 --- a/server/app/interfaces/base.py +++ b/server/app/interfaces/base.py @@ -129,7 +129,7 @@ def __init__(self, *args, content_type="application/xml", **kwargs): def serialize(self, obj: ResponseData, paging_metadata: Optional[PagingMetadata], stripped: bool) -> str: root_elem = etree.Element("response", nsmap=XML_NS_MAP) - if paging_metadata is not None: + if paging_metadata is not None and paging_metadata.cursor is not None: root_elem.set("cursor", str(paging_metadata.cursor)) if isinstance(obj, Result): result_elem = self.result_to_xml(obj, **XML_NS_MAP) diff --git a/server/test/interfaces/format_utils.py b/server/test/interfaces/format_utils.py index 661741b5..078aede9 100644 --- a/server/test/interfaces/format_utils.py +++ b/server/test/interfaces/format_utils.py @@ -215,10 +215,7 @@ def result_success(self, response: TestResponse) -> bool: return success_elem is None or success_elem.text == "true" def next_cursor(self, response: TestResponse) -> Optional[str]: - # The cursor is an attribute on the root; it is unconditionally serialized, so a - # missing next page shows up as the literal string "None" rather than an absent attribute. - cursor = self._root(response).get("cursor") - return cursor if cursor not in (None, "None") else None + return self._root(response).get("cursor") class AssertResponseMixin(unittest.TestCase): diff --git a/server/test/interfaces/test_base.py b/server/test/interfaces/test_base.py index afeed463..ecdacca3 100644 --- a/server/test/interfaces/test_base.py +++ b/server/test/interfaces/test_base.py @@ -246,6 +246,29 @@ def test_paging_metadata_no_cursor(self): self.assertIsNone(parsed_body.get("cursor")) + def test_pagination_cursor_attribute(self) -> None: + cases = ( + (None, None), + (base.PagingMetadata(cursor=None), None), + (base.PagingMetadata(cursor="asdf"), "asdf"), + (base.PagingMetadata(cursor="None"), "None"), + ) + for response_type in (base.XmlResponse, base.XmlResponseAlt): + for objects in ([], [model.Submodel(id_="https://example.org/Example_Submodel")]): + for metadata, expected_cursor in cases: + with self.subTest( + response_type=response_type.__name__, + objects=objects, + expected_cursor=expected_cursor, + metadata=metadata, + ): + response = response_type(obj=objects, paging_metadata=metadata) + parsed_body = etree.fromstring(response.get_data()) + if expected_cursor is None: + self.assertNotIn("cursor", parsed_body.attrib) + else: + self.assertEqual(expected_cursor, parsed_body.get("cursor")) + def test_result(self): response = base.XmlResponse( base.Result(False, [base.Message("BAD_CODE", "test", base.MessageType.ERROR, datetime.datetime.now())])