diff --git a/packtools/sps/models/v2/article_xref.py b/packtools/sps/models/v2/article_xref.py index a963c50b2..ce8b5c521 100644 --- a/packtools/sps/models/v2/article_xref.py +++ b/packtools/sps/models/v2/article_xref.py @@ -155,3 +155,34 @@ def xrefs_by_rid(self): response.setdefault(rid, []) response[rid].append(xref_data) return response + + def all_xrefs(self): + """Returns a list of data dicts for all elements in the document.""" + result = [] + for xref_node in self.xml_tree.xpath(".//xref"): + xref = Xref(xref_node) + data = xref.data + data["xml"] = xref.xml + # Check if element has text content (for self-closing detection) + content = " ".join(xref_node.xpath(".//text()")).strip() + data["has_text_content"] = bool(content) + result.append(data) + return result + + def all_ids(self): + """Returns a set of all @id attribute values in the document.""" + ids = set() + for node in self.xml_tree.xpath(".//*[@id]"): + id_val = node.get("id") + if id_val: + ids.add(id_val) + return ids + + def transcript_sections(self): + """Returns a list of @id values for elements.""" + result = [] + for node in self.xml_tree.xpath('.//sec[@sec-type="transcript"]'): + sec_id = node.get("id") + if sec_id: + result.append(sec_id) + return result diff --git a/packtools/sps/validation/article_xref.py b/packtools/sps/validation/article_xref.py index d290df7f2..5456ed6c8 100644 --- a/packtools/sps/validation/article_xref.py +++ b/packtools/sps/validation/article_xref.py @@ -2,29 +2,59 @@ from packtools.sps.validation.utils import build_response +# Allowed values for @ref-type per SPS 1.10 +REF_TYPES = [ + "aff", + "app", + "author-notes", + "bibr", + "bio", + "boxed-text", + "contrib", + "corresp", + "disp-formula", + "fig", + "fn", + "list", + "sec", + "supplementary-material", + "table", + "table-fn", +] + + class ArticleXrefValidation: def __init__(self, xml_tree, params=None): self.xml_tree = xml_tree self.xml_cross_refs = XMLCrossReference(xml_tree) - + # Get default parameters and update with provided params if any self.params = self.get_default_params() if params: self.params.update(params) self.xrefs_by_rid = self.xml_cross_refs.xrefs_by_rid() - + ids = set(self.xml_cross_refs.elems_by_id("*").keys()) rids = set(self.xrefs_by_rid.keys()) self.missing_xrefs = list(ids - rids) self.missing_elems = list(rids - ids) + self._parent = { + "parent": "article", + "parent_id": None, + "parent_article_type": self.xml_tree.get("article-type"), + "parent_lang": self.xml_tree.get( + "{http://www.w3.org/XML/1998/namespace}lang" + ), + } + @staticmethod def get_default_params(): """ Returns the default parameters for validation. - + Returns ------- dict @@ -37,7 +67,7 @@ def get_default_params(): "table-wrap", "ref", "aff", - "corresp" + "corresp", ), "attrib_name_and_value_requires_xref": [ {"name": "sec-type", "value": "transcript"} @@ -45,10 +75,243 @@ def get_default_params(): "xref_rid_error_level": "ERROR", "element_id_error_level": "ERROR", "attrib_name_and_value_requires_xref_error_level": "CRITICAL", - "required_id_error_level": "CRITICAL", - "required_rid_error_level": "CRITICAL", + "rid_presence_error_level": "CRITICAL", + "ref_type_presence_error_level": "CRITICAL", + "ref_type_value_error_level": "ERROR", + "bibr_presence_error_level": "ERROR", + "rid_id_correspondence_error_level": "ERROR", + "transcript_xref_error_level": "WARNING", + "aff_self_closing_error_level": "INFO", + "ref_type_list": REF_TYPES, } + def validate_rid_presence(self): + """ + Validates that all elements have a non-empty @rid attribute. + + Yields + ------ + dict + Validation result for each element. + """ + error_level = self.params["rid_presence_error_level"] + for xref_data in self.xml_cross_refs.all_xrefs(): + rid = xref_data.get("rid") + is_valid = bool(rid and rid.strip()) + yield build_response( + title="xref @rid", + parent=self._parent, + item="xref", + sub_item="@rid", + validation_type="exist", + is_valid=is_valid, + expected="@rid attribute with a non-empty value", + obtained=rid, + advice=f'Provide a valid @rid attribute for ', + data=xref_data, + error_level=error_level, + advice_text='Provide a valid @rid attribute for ', + advice_params={}, + ) + + def validate_ref_type_presence(self): + """ + Validates that all elements have a non-empty @ref-type attribute. + + Yields + ------ + dict + Validation result for each element. + """ + error_level = self.params["ref_type_presence_error_level"] + for xref_data in self.xml_cross_refs.all_xrefs(): + ref_type = xref_data.get("ref-type") + is_valid = bool(ref_type and ref_type.strip()) + yield build_response( + title="xref @ref-type", + parent=self._parent, + item="xref", + sub_item="@ref-type", + validation_type="exist", + is_valid=is_valid, + expected="@ref-type attribute with a non-empty value", + obtained=ref_type, + advice=f'Provide a valid @ref-type attribute for ', + data=xref_data, + error_level=error_level, + advice_text='Provide a valid @ref-type attribute for ', + advice_params={}, + ) + + def validate_ref_type_value(self): + """ + Validates that @ref-type values are in the list of allowed values. + + Yields + ------ + dict + Validation result for each element with a @ref-type. + """ + error_level = self.params["ref_type_value_error_level"] + ref_type_list = self.params.get("ref_type_list", REF_TYPES) + for xref_data in self.xml_cross_refs.all_xrefs(): + ref_type = xref_data.get("ref-type") + if not ref_type or not ref_type.strip(): + continue + is_valid = ref_type in ref_type_list + yield build_response( + title="xref @ref-type value", + parent=self._parent, + item="xref", + sub_item="@ref-type", + validation_type="value in list", + is_valid=is_valid, + expected=str(ref_type_list), + obtained=ref_type, + advice=f'Replace "{ref_type}" with one of the allowed values: {ref_type_list}', + data=xref_data, + error_level=error_level, + advice_text='Replace "{ref_type}" with one of the allowed values: {ref_type_list}', + advice_params={"ref_type": ref_type, "ref_type_list": str(ref_type_list)}, + ) + + def validate_bibr_presence(self): + """ + Validates that the document contains at least one . + SciELO Brasil criterion. + + Yields + ------ + dict + Validation result for the bibr presence check. + """ + error_level = self.params["bibr_presence_error_level"] + all_xrefs = self.xml_cross_refs.all_xrefs() + bibr_count = sum(1 for x in all_xrefs if x.get("ref-type") == "bibr") + is_valid = bibr_count > 0 + yield build_response( + title="xref @ref-type bibr", + parent=self._parent, + item="xref", + sub_item='@ref-type="bibr"', + validation_type="exist", + is_valid=is_valid, + expected='at least one ', + obtained=f"{bibr_count} found", + advice='Add at least one to the document (SciELO Brasil criterion)', + data={"bibr_count": bibr_count}, + error_level=error_level, + advice_text='Add at least one to the document (SciELO Brasil criterion)', + advice_params={"bibr_count": str(bibr_count)}, + ) + + def validate_rid_has_corresponding_id(self): + """ + Validates that every @rid in has a corresponding @id in the document. + + Yields + ------ + dict + Validation result for each with a @rid. + """ + error_level = self.params["rid_id_correspondence_error_level"] + all_ids = self.xml_cross_refs.all_ids() + for xref_data in self.xml_cross_refs.all_xrefs(): + rid = xref_data.get("rid") + if not rid or not rid.strip(): + continue + rid = rid.strip() + is_valid = rid in all_ids + yield build_response( + title="xref @rid id correspondence", + parent=self._parent, + item="xref", + sub_item="@rid", + validation_type="match", + is_valid=is_valid, + expected=f'element with @id="{rid}"', + obtained=rid if is_valid else None, + advice=f'@rid="{rid}" in has no corresponding @id in the document', + data=xref_data, + error_level=error_level, + advice_text='@rid="{rid}" in has no corresponding @id in the document', + advice_params={"rid": rid}, + ) + + def validate_transcript_xref(self): + """ + Validates that when exists, there is a + referencing it. + + Yields + ------ + dict or None + Validation result, or None if no transcript sections exist. + """ + error_level = self.params["transcript_xref_error_level"] + transcript_ids = self.xml_cross_refs.transcript_sections() + if not transcript_ids: + return + + xrefs_by_rid = self.xrefs_by_rid + for sec_id in transcript_ids: + xrefs = xrefs_by_rid.get(sec_id) + has_sec_xref = False + if xrefs: + has_sec_xref = any( + x.get("ref-type") == "sec" for x in xrefs + ) + yield build_response( + title="xref for transcript section", + parent=self._parent, + item="xref", + sub_item='@ref-type="sec"', + validation_type="match", + is_valid=has_sec_xref, + expected=f'', + obtained=xrefs if has_sec_xref else None, + advice=f'Add to reference the transcript section', + data={"transcript_sec_id": sec_id}, + error_level=error_level, + advice_text='Add to reference the transcript section', + advice_params={"sec_id": sec_id}, + ) + + def validate_aff_self_closing(self): + """ + Validates that without text content uses + self-closing format (INFO-level recommendation). + + Yields + ------ + dict + Validation result for each aff xref without text content. + """ + error_level = self.params["aff_self_closing_error_level"] + for xref_data in self.xml_cross_refs.all_xrefs(): + ref_type = xref_data.get("ref-type") + if ref_type != "aff": + continue + has_text = xref_data.get("has_text_content", True) + if has_text: + continue + rid = xref_data.get("rid", "") + yield build_response( + title="xref aff self-closing", + parent=self._parent, + item="xref", + sub_item="@ref-type", + validation_type="format", + is_valid=False, + expected=f'', + obtained=xref_data.get("xml", ""), + advice=f'For @ref-type="aff" without text content, use self-closing: ', + data=xref_data, + error_level=error_level, + advice_text='For @ref-type="aff" without text content, use self-closing: ', + advice_params={"rid": rid}, + ) + def validate_xref_rid_has_corresponding_element_id(self): """ Checks if all `rid` attributes (source) in `` elements have corresponding `id` attributes (destination) @@ -64,22 +327,14 @@ def validate_xref_rid_has_corresponding_element_id(self): for xref in xrefs: element_data = elements_by_id.get(rid) is_valid = bool(element_data) - element_name = xref.get("element_name") - xref_content = xref.get("content") + element_name = xref.get("elem_name") advice = ( - f'Found {xref.get("xml")}, but not found the corresponding {xref.get("elem_xml")}' + f'Found {xref.get("tag_and_attribs")}, but not found the corresponding {xref.get("elem_xml")}' ) yield build_response( title=f' is linked to {element_name}', - parent={ - "parent": "article", - "parent_id": None, - "parent_article_type": self.xml_tree.get("article-type"), - "parent_lang": self.xml_tree.get( - "{http://www.w3.org/XML/1998/namespace}lang" - ), - }, + parent=self._parent, item="xref", sub_item="@rid", validation_type="match", @@ -87,10 +342,18 @@ def validate_xref_rid_has_corresponding_element_id(self): expected=f'{element_name} which id="{rid}"', obtained=element_data, advice=advice, - data={"xref": xref, "element": element_data, "missing_xrefs": self.missing_xrefs, "missing_elems": self.missing_elems}, + data={ + "xref": xref, + "element": element_data, + "missing_xrefs": self.missing_xrefs, + "missing_elems": self.missing_elems, + }, error_level=self.params["xref_rid_error_level"], - advice_text='Found {xml}, but not found the corresponding {elem_xml}', - advice_params={"xml": xref.get("xml"), "elem_xml": xref.get("elem_xml")}, + advice_text='Found {tag_and_attribs}, but not found the corresponding {elem_xml}', + advice_params={ + "tag_and_attribs": xref.get("tag_and_attribs"), + "elem_xml": xref.get("elem_xml"), + }, ) def validate_element_id_has_corresponding_xref_rid(self): @@ -109,10 +372,10 @@ def validate_element_id_has_corresponding_xref_rid(self): elements_requires_xref_rid = set(elements_requires_xref_rid) for element_name in elements_requires_xref_rid: - for id, elems in self.xml_cross_refs.elems_by_id(element_name).items(): + for id_val, elems in self.xml_cross_refs.elems_by_id(element_name).items(): for elem_data in elems: tag = elem_data.get("tag") - xrefs = xrefs_by_rid.get(id) + xrefs = xrefs_by_rid.get(id_val) is_valid = bool(xrefs) tag_and_attribs = elem_data.get("tag_and_attribs") xref_xml = elem_data.get("xref_xml") @@ -130,7 +393,7 @@ def validate_element_id_has_corresponding_xref_rid(self): advice_params = { "tag_and_attribs": tag_and_attribs, "xref_xml": xref_xml, - "label": label + "label": label, } else: advice = ( @@ -141,7 +404,7 @@ def validate_element_id_has_corresponding_xref_rid(self): ) advice_params = { "tag_and_attribs": tag_and_attribs, - "xref_xml": xref_xml + "xref_xml": xref_xml, } yield build_response( @@ -159,7 +422,12 @@ def validate_element_id_has_corresponding_xref_rid(self): expected=xref_xml, obtained=xrefs, advice=advice, - data={"element": elem_data, "xref": xrefs, "missing_xrefs": self.missing_xrefs, "missing_elems": self.missing_elems}, + data={ + "element": elem_data, + "xref": xrefs, + "missing_xrefs": self.missing_xrefs, + "missing_elems": self.missing_elems, + }, error_level=error_level, advice_text=advice_text, advice_params=advice_params, @@ -168,7 +436,7 @@ def validate_element_id_has_corresponding_xref_rid(self): def validate_attrib_name_and_value_has_corresponding_xref(self): """ Checks if sections with specific sec-type attributes have corresponding xref references. - Only validates sections whose sec-type is in the sec_type_requires_rid list. + Only validates sections whose sec-type is in the attrib_name_and_value_requires_xref list. Yields ------ @@ -178,23 +446,27 @@ def validate_attrib_name_and_value_has_corresponding_xref(self): attribs = self.params["attrib_name_and_value_requires_xref"] or [] error_level = self.params["attrib_name_and_value_requires_xref_error_level"] - for elems in self.xml_cross_refs.elems_by_id(attribs=attribs).values(): + for elem_id, elems in self.xml_cross_refs.elems_by_id(attribs=attribs).items(): for elem_data in elems: tag = elem_data.get("tag") - - xrefs = self.xrefs_by_rid.get(id) + + xrefs = self.xrefs_by_rid.get(elem_id) is_valid = bool(xrefs) xref_xml = elem_data.get("xref_xml") - xml = elem_data.get("xml") tag_and_attribs = elem_data.get("tag_and_attribs") advice = ( - f'Found {xml}, but no corresponding {self.xref_xml} was found. ' - f'Mark the {xml} cross-references using {self.xref_xml}' + f'Found {tag_and_attribs}, but no corresponding {xref_xml} was found. ' + f'Mark the {tag_and_attribs} cross-references using {xref_xml}' ) - + yield build_response( title=f'{tag_and_attribs} is linked to ', - parent=elem_data, + parent={ + "parent": elem_data.get("parent"), + "parent_id": elem_data.get("parent_id"), + "parent_article_type": elem_data.get("parent_article_type"), + "parent_lang": elem_data.get("parent_lang"), + }, item=tag, sub_item="@id", validation_type="match", @@ -209,4 +481,10 @@ def validate_attrib_name_and_value_has_corresponding_xref(self): "missing_elems": self.missing_elems, }, error_level=error_level, + advice_text='Found {tag_and_attribs}, but no corresponding {xref_xml} was found. ' + 'Mark the {tag_and_attribs} cross-references using {xref_xml}', + advice_params={ + "tag_and_attribs": tag_and_attribs, + "xref_xml": xref_xml, + }, ) \ No newline at end of file diff --git a/packtools/sps/validation/xml_validations.py b/packtools/sps/validation/xml_validations.py index d62b5f9f9..a9f1a474b 100644 --- a/packtools/sps/validation/xml_validations.py +++ b/packtools/sps/validation/xml_validations.py @@ -157,8 +157,19 @@ def validate_article_toc_sections(xmltree, params): def validate_id_and_rid_match(xmltree, params): - id_and_rid_match_rules = params["id_and_rid_match_rules"] - validator = ArticleXrefValidation(xmltree, id_and_rid_match_rules) + id_and_rid_match_rules = params.get("id_and_rid_match_rules") or {} + xref_rules = params.get("xref_rules") or {} + merged_rules = {} + merged_rules.update(id_and_rid_match_rules) + merged_rules.update(xref_rules) + validator = ArticleXrefValidation(xmltree, merged_rules) + yield from validator.validate_rid_presence() + yield from validator.validate_ref_type_presence() + yield from validator.validate_ref_type_value() + yield from validator.validate_bibr_presence() + yield from validator.validate_rid_has_corresponding_id() + yield from validator.validate_transcript_xref() + yield from validator.validate_aff_self_closing() yield from validator.validate_xref_rid_has_corresponding_element_id() yield from validator.validate_element_id_has_corresponding_xref_rid() yield from validator.validate_attrib_name_and_value_has_corresponding_xref() diff --git a/packtools/sps/validation_rules/xref_rules.json b/packtools/sps/validation_rules/xref_rules.json new file mode 100644 index 000000000..7d53ee7af --- /dev/null +++ b/packtools/sps/validation_rules/xref_rules.json @@ -0,0 +1,43 @@ +{ + "xref_rules": { + "xref_rid_error_level": "ERROR", + "element_id_error_level": "ERROR", + "attrib_name_and_value_requires_xref_error_level": "CRITICAL", + "rid_presence_error_level": "CRITICAL", + "ref_type_presence_error_level": "CRITICAL", + "ref_type_value_error_level": "ERROR", + "bibr_presence_error_level": "ERROR", + "rid_id_correspondence_error_level": "ERROR", + "transcript_xref_error_level": "WARNING", + "aff_self_closing_error_level": "INFO", + "elements_requires_xref_rid": [ + "fig", + "disp-formula", + "table-wrap", + "ref", + "aff", + "corresp" + ], + "attrib_name_and_value_requires_xref": [ + {"name": "sec-type", "value": "transcript"} + ], + "ref_type_list": [ + "aff", + "app", + "author-notes", + "bibr", + "bio", + "boxed-text", + "contrib", + "corresp", + "disp-formula", + "fig", + "fn", + "list", + "sec", + "supplementary-material", + "table", + "table-fn" + ] + } +} diff --git a/tests/sps/validation/test_article_xref.py b/tests/sps/validation/test_article_xref.py index 1b7f13bb9..847be8565 100644 --- a/tests/sps/validation/test_article_xref.py +++ b/tests/sps/validation/test_article_xref.py @@ -4,317 +4,1023 @@ from packtools.sps.validation.article_xref import ArticleXrefValidation -class ArticleXrefValidationTest(TestCase): - - def test_validate_rids_matches(self): - self.maxDiff = None - self.xml_tree = etree.fromstring( - """ -
- - -

1

- -

affiliation

-
- -

1

- -

figure

-
- -

1

- -

table

-
-
-
-
- """ - ) - obtained = list(ArticleXrefValidation(self.xml_tree).validate_xref_rid_has_corresponding_element_id()) - - expected = [ - { - "title": "xref[@rid] -> *[@id]", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "item": "xref", - "sub_item": "@rid", - "validation_type": "match", - "response": "OK", - "expected_value": "aff1", - "got_value": "aff1", - "message": "Got aff1, expected aff1", - "advice": None, - "data": {"element_name": "aff", "ref-type": "aff", "rid": "aff1", "text": "1"}, - }, - { - "title": "xref[@rid] -> *[@id]", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "item": "xref", - "sub_item": "@rid", - "validation_type": "match", - "response": "OK", - "expected_value": "fig1", - "got_value": "fig1", - "message": "Got fig1, expected fig1", - "advice": None, - "data": {"element_name": "fig", "ref-type": "fig", "rid": "fig1", "text": "1"}, - }, - { - "title": "xref[@rid] -> *[@id]", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "item": "xref", - "sub_item": "@rid", - "validation_type": "match", - "response": "OK", - "expected_value": "table1", - "got_value": "table1", - "message": "Got table1, expected table1", - "advice": None, - "data": {"element_name": "table-wrap", "ref-type": "table", "rid": "table1", "text": "1"}, - }, - ] - self.assertEqual(len(obtained), 3) - for i, item in enumerate(expected): - with self.subTest(i): - self.assertDictEqual(obtained[i], item) - - def test_validate_rids_no_matches(self): - self.maxDiff = None - self.xmltree = etree.fromstring( - """ -
- - -

1

- -

affiliation

-
- -

1

- -

figure

-
- -

1

-
-
-
- """ - ) - self.article_xref = ArticleXrefValidation(self.xmltree) - - expected = [ - { - "title": "xref[@rid] -> *[@id]", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "item": "xref", - "sub_item": "@rid", - "validation_type": "match", - "response": "OK", - "expected_value": "aff1", - "got_value": "aff1", - "message": "Got aff1, expected aff1", - "advice": None, - "data": {"element_name": "aff", "ref-type": "aff", "rid": "aff1", "text": "1"}, - }, - { - "title": "xref[@rid] -> *[@id]", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "item": "xref", - "sub_item": "@rid", - "validation_type": "match", - "response": "OK", - "expected_value": "fig1", - "got_value": "fig1", - "message": "Got fig1, expected fig1", - "advice": None, - "data": {"element_name": "fig", "ref-type": "fig", "rid": "fig1", "text": "1"}, - }, - { - "title": "xref[@rid] -> *[@id]", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "item": "xref", - "sub_item": "@rid", - "validation_type": "match", - "response": "ERROR", - "expected_value": "table1", - "got_value": None, - "message": "Got None, expected table1", - "advice": 'Check if xref[@rid="table1"] is correct or insert the missing table-wrap[@id="table1"]', - "data": {"element_name": "table-wrap", "ref-type": "table", "rid": "table1", "text": "1"}, - }, - ] - obtained = list(self.article_xref.validate_xref_rid_has_corresponding_element_id()) - self.assertEqual(len(obtained), 3) - for i, item in enumerate(expected): - with self.subTest(i): - self.assertDictEqual(obtained[i], item) - - def test_validate_ids_matches(self): - self.maxDiff = None - self.xmltree = etree.fromstring( - """ -
- - -

1

- -

affiliation

-
- -

1

- -

figure

-
- -

1

- -

table

-
-
-
-
- """ - ) - self.article_xref = ArticleXrefValidation(self.xmltree) - - obtained = list(self.article_xref.validate_element_id_has_corresponding_xref_rid()) - - self.assertEqual(["OK", "OK", "OK"], [item["response"] for item in obtained]) - - expected = [ - None, - None, - None - ] - for i, item in enumerate(obtained): - self.assertEqual(expected[i], item["advice"]) - - def test_validate_ids_no_matches(self): - self.maxDiff = None - self.xmltree = etree.fromstring( - """ -
- - -

1

- -

affiliation

-
- -

1

- -

figure

-
- - -

table

-
-
-
-
- """ - ) - self.article_xref = ArticleXrefValidation(self.xmltree) - - expected = [ - { - "title": "*[@id] -> xref[@rid]", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "item": "aff", - "sub_item": "@id", - "validation_type": "match", - "response": "OK", - "expected_value": "aff1", - "got_value": "aff1", - "message": "Got aff1, expected aff1", - "advice": None, - "data": { - "id": "aff1", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "tag": "aff", - }, - }, - { - "title": "*[@id] -> xref[@rid]", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "item": "fig", - "sub_item": "@id", - "validation_type": "match", - "response": "OK", - "expected_value": "fig1", - "got_value": "fig1", - "message": "Got fig1, expected fig1", - "advice": None, - "data": { - "id": "fig1", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "tag": "fig", - }, - }, - { - "title": "*[@id] -> xref[@rid]", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "item": "table-wrap", - "sub_item": "@id", - "validation_type": "match", - "response": "ERROR", - "expected_value": "table1", - "got_value": None, - "message": "Got None, expected table1", - "advice": 'Check if table-wrap[@id="table1"] is correct or insert the missing xref[@rid="table1"]', - "data": { - "id": "table1", - "parent": "article", - "parent_article_type": "research-article", - "parent_id": None, - "parent_lang": "pt", - "tag": "table-wrap", - }, - }, - ] - - obtained = list(self.article_xref.validate_element_id_has_corresponding_xref_rid()) - - for i, item in enumerate(expected): - with self.subTest(i): - self.assertDictEqual(obtained[i], item) +def filter_results(results): + """Remove None values from validation results.""" + return [r for r in results if r is not None] + + +class TestValidateRidPresence(TestCase): + """Tests for validate_rid_presence: @rid is mandatory in .""" + + def test_rid_present(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_rid_missing(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + + def test_rid_empty(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + + def test_rid_whitespace_only(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + + def test_multiple_xrefs_one_missing_rid(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + '

Figure 1

' + "" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(len(results), 2) + responses = [r["response"] for r in results] + self.assertIn("OK", responses) + self.assertIn("CRITICAL", responses) + + +class TestValidateRefTypePresence(TestCase): + """Tests for validate_ref_type_presence: @ref-type is mandatory in .""" + + def test_ref_type_present(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_ref_type_missing(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + + def test_ref_type_empty(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + + def test_both_attributes_empty(self): + xml_tree = etree.fromstring( + '
' + "" + '

reference

' + "" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + ref_type_results = list(validator.validate_ref_type_presence()) + rid_results = list(validator.validate_rid_presence()) + self.assertEqual(ref_type_results[0]["response"], "CRITICAL") + self.assertEqual(rid_results[0]["response"], "CRITICAL") + + +class TestValidateRefTypeValue(TestCase): + """Tests for validate_ref_type_value: @ref-type must be a valid value.""" + + def test_valid_aff(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_app(self): + xml_tree = etree.fromstring( + '
' + "" + '

Appendix

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_author_notes(self): + xml_tree = etree.fromstring( + '
' + "" + '

*

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_bibr(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_bio(self): + xml_tree = etree.fromstring( + '
' + "" + '

Bio

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_boxed_text(self): + xml_tree = etree.fromstring( + '
' + "" + '

Box 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_contrib(self): + xml_tree = etree.fromstring( + '
' + "" + '

Author

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_corresp(self): + xml_tree = etree.fromstring( + '
' + "" + '

*

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_disp_formula(self): + xml_tree = etree.fromstring( + '
' + "" + '

Eq. 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_fig(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_fn(self): + xml_tree = etree.fromstring( + '
' + "" + '

*

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_list(self): + xml_tree = etree.fromstring( + '
' + "" + '

List 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_sec(self): + xml_tree = etree.fromstring( + '
' + "" + '

Section 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_supplementary_material(self): + xml_tree = etree.fromstring( + '
' + "" + '

Suppl 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_table(self): + xml_tree = etree.fromstring( + '
' + "" + '

Table 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_table_fn(self): + xml_tree = etree.fromstring( + '
' + "" + '

*

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_invalid_image(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "ERROR") + + def test_invalid_reference(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "ERROR") + + def test_invalid_uppercase_fig(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "ERROR") + + def test_invalid_underscore_author_notes(self): + xml_tree = etree.fromstring( + '
' + "" + '

*

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "ERROR") + + def test_skips_empty_ref_type(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(len(results), 0) + + +class TestValidateBibrPresence(TestCase): + """Tests for validate_bibr_presence: at least one xref with ref-type='bibr' required.""" + + def test_has_bibr(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "" + "" + 'Citation' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_bibr_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_no_bibr(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_bibr_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + + def test_multiple_bibr(self): + xml_tree = etree.fromstring( + '
' + "" + '

1, ' + '2, ' + '3

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_bibr_presence()) + self.assertEqual(results[0]["response"], "OK") + + def test_no_xref_at_all(self): + xml_tree = etree.fromstring( + '
' + "

No cross-references.

" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_bibr_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + + def test_editorial_without_bibr(self): + xml_tree = etree.fromstring( + '
' + "

Editorial.

" + "" + 'Ref' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_bibr_presence()) + self.assertEqual(results[0]["response"], "ERROR") + + def test_has_other_xref_but_no_bibr(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + '

Table 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_bibr_presence()) + self.assertEqual(results[0]["response"], "ERROR") + + +class TestValidateRidHasCorrespondingId(TestCase): + """Tests for validate_rid_has_corresponding_id: @rid must point to existing @id.""" + + def test_rid_matches_id(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + '' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_rid_no_matching_id(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 999

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + + def test_multiple_rids_all_match(self): + xml_tree = etree.fromstring( + '
' + "" + '

1, ' + '2

' + "" + "" + 'R1' + 'R2' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + for r in results: + self.assertEqual(r["response"], "OK") + + def test_multiple_rids_some_no_match(self): + xml_tree = etree.fromstring( + '
' + "" + '

Fig 1 and ' + 'Table 999

' + '' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + responses = [r["response"] for r in results] + self.assertIn("OK", responses) + self.assertIn("ERROR", responses) + + def test_rid_to_ref(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "" + "" + 'Citation' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + self.assertEqual(results[0]["response"], "OK") + + def test_rid_to_aff(self): + xml_tree = etree.fromstring( + '
' + "" + '' + '1' + "" + 'University' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + self.assertEqual(results[0]["response"], "OK") + + def test_rid_to_table(self): + xml_tree = etree.fromstring( + '
' + "" + '

Table 1

' + '' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + self.assertEqual(results[0]["response"], "OK") + + def test_rid_to_sec(self): + xml_tree = etree.fromstring( + '
' + "" + '

Section 1

' + 'Section 1' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + self.assertEqual(results[0]["response"], "OK") + + def test_skips_empty_rid(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + self.assertEqual(len(results), 0) + + def test_multiple_rids_all_missing(self): + xml_tree = etree.fromstring( + '
' + "" + '

1, ' + '2, ' + '3

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + self.assertEqual(len(results), 3) + for r in results: + self.assertEqual(r["response"], "ERROR") + + def test_bibr_rid_without_ref_list(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_has_corresponding_id()) + self.assertEqual(results[0]["response"], "ERROR") + + +class TestValidateTranscriptXref(TestCase): + """Tests for validate_transcript_xref: transcript sections need xref.""" + + def test_no_transcript_section(self): + xml_tree = etree.fromstring( + '
' + "" + "Introduction

Content

" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = filter_results(list(validator.validate_transcript_xref())) + self.assertEqual(len(results), 0) + + def test_transcript_with_xref(self): + xml_tree = etree.fromstring( + '
' + "" + '' + "" + '' + "" + '' + "Transcript

Content

" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = filter_results(list(validator.validate_transcript_xref())) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_transcript_without_xref(self): + xml_tree = etree.fromstring( + '
' + "" + '' + "" + "" + '' + "Transcript

Content

" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = filter_results(list(validator.validate_transcript_xref())) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "WARNING") + + +class TestValidateAffSelfClosing(TestCase): + """Tests for validate_aff_self_closing: aff xref without text should be self-closing.""" + + def test_aff_with_text(self): + xml_tree = etree.fromstring( + '
' + "" + '' + '1' + "" + 'University' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_aff_self_closing()) + self.assertEqual(len(results), 0) + + def test_aff_self_closing(self): + xml_tree = etree.fromstring( + '
' + "" + '' + '' + "" + 'University' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_aff_self_closing()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "INFO") + + def test_aff_empty_not_self_closing(self): + xml_tree = etree.fromstring( + '
' + "" + '' + '' + "" + 'University' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_aff_self_closing()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "INFO") + + def test_non_aff_not_validated(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_aff_self_closing()) + self.assertEqual(len(results), 0) + + +class TestValidateXrefRidHasCorrespondingElementId(TestCase): + """Tests for the existing validate_xref_rid_has_corresponding_element_id method.""" + + def test_all_rids_match(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + '

affiliation

' + '

1

' + '

figure

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_xref_rid_has_corresponding_element_id()) + for r in results: + self.assertEqual(r["response"], "OK") + + def test_rid_no_match(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_xref_rid_has_corresponding_element_id()) + error_results = [r for r in results if r["response"] == "ERROR"] + self.assertTrue(len(error_results) > 0) + + +class TestValidateElementIdHasCorrespondingXrefRid(TestCase): + """Tests for the existing validate_element_id_has_corresponding_xref_rid method.""" + + def test_all_ids_have_xref(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + '

affiliation

' + '

1

' + '

figure

' + '

1

' + '

table

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_element_id_has_corresponding_xref_rid()) + for r in results: + self.assertEqual(r["response"], "OK") + + def test_id_without_xref(self): + xml_tree = etree.fromstring( + '
' + "" + '

table

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_element_id_has_corresponding_xref_rid()) + error_results = [r for r in results if r["response"] == "ERROR"] + self.assertTrue(len(error_results) > 0) + + +class TestValidateAttribNameAndValueHasCorrespondingXref(TestCase): + """Tests for validate_attrib_name_and_value_has_corresponding_xref.""" + + def test_transcript_with_xref(self): + xml_tree = etree.fromstring( + '
' + "" + '' + '' + "Transcript

Content

" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_attrib_name_and_value_has_corresponding_xref()) + ok_results = [r for r in results if r["response"] == "OK"] + self.assertTrue(len(ok_results) > 0) + + def test_transcript_without_xref(self): + xml_tree = etree.fromstring( + '
' + "" + '' + "Transcript

Content

" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_attrib_name_and_value_has_corresponding_xref()) + error_results = [r for r in results if r["response"] != "OK"] + self.assertTrue(len(error_results) > 0) + + +class TestMultipleXrefs(TestCase): + """Tests for documents with multiple xrefs.""" + + def test_no_xref(self): + xml_tree = etree.fromstring( + '
' + "

No xrefs here.

" + ) + validator = ArticleXrefValidation(xml_tree) + rid_results = list(validator.validate_rid_presence()) + self.assertEqual(len(rid_results), 0) + + def test_single_bibr_xref(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "" + "" + 'Ref' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + bibr_results = list(validator.validate_bibr_presence()) + self.assertEqual(bibr_results[0]["response"], "OK") + + def test_multiple_different_xrefs(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + '

Figure 1

' + '

Table 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(len(results), 3) + for r in results: + self.assertEqual(r["response"], "OK") + + +class TestXrefInDifferentContexts(TestCase): + """Tests for xref in different parent elements.""" + + def test_xref_in_p(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(results[0]["response"], "OK") + + def test_xref_in_td(self): + xml_tree = etree.fromstring( + '
' + "" + '' + '' + "
1
" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(results[0]["response"], "OK") + + def test_xref_in_th(self): + xml_tree = etree.fromstring( + '
' + "" + '' + '' + "
*
" + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(results[0]["response"], "OK") + + def test_xref_in_contrib(self): + xml_tree = etree.fromstring( + '
' + "" + '' + 'SilvaJ' + '1' + "" + 'Univ' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(results[0]["response"], "OK") + + def test_xref_in_article_title(self): + xml_tree = etree.fromstring( + '
' + "" + "" + 'Title *' + "" + "" + "

Content

" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertEqual(results[0]["response"], "OK") + + +class TestEdgeCases(TestCase): + """Tests for edge cases.""" + + def test_xref_with_complex_content(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "" + "" + 'Ref' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + rid_results = list(validator.validate_rid_presence()) + self.assertEqual(rid_results[0]["response"], "OK") + bibr_results = list(validator.validate_bibr_presence()) + self.assertEqual(bibr_results[0]["response"], "OK") + + def test_xref_with_sup_inside(self): + xml_tree = etree.fromstring( + '
' + "" + '

Text1

' + "" + "" + 'Smith. 2020.' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(results[0]["response"], "OK") + + def test_citation_range(self): + xml_tree = etree.fromstring( + '
' + "" + '

Studies 1-' + '5

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_ref_type_value()) + self.assertEqual(len(results), 2) + for r in results: + self.assertEqual(r["response"], "OK") + + +class TestResponseFormat(TestCase): + """Tests for the response format structure.""" + + def test_response_has_required_keys(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + self.assertTrue(len(results) > 0) + expected_keys = { + "title", "parent", "parent_id", "parent_article_type", + "parent_lang", "item", "sub_item", "validation_type", + "response", "expected_value", "got_value", "message", + "msg_text", "msg_params", "advice", "adv_text", + "adv_params", "data", + } + self.assertEqual(set(results[0].keys()), expected_keys) + + def test_i18n_fields_present(self): + xml_tree = etree.fromstring( + '
' + "" + '

Figure 1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + result = results[0] + self.assertIn("msg_text", result) + self.assertIn("msg_params", result) + self.assertIn("adv_text", result) + self.assertIn("adv_params", result) + self.assertEqual(result["response"], "CRITICAL") + self.assertIsNotNone(result["adv_text"]) + + def test_ok_response_has_null_advice(self): + xml_tree = etree.fromstring( + '
' + "" + '

1

' + "
" + ) + validator = ArticleXrefValidation(xml_tree) + results = list(validator.validate_rid_presence()) + result = results[0] + self.assertEqual(result["response"], "OK") + self.assertIsNone(result["advice"]) + self.assertIsNone(result["adv_text"]) + self.assertIsNone(result["adv_params"])