diff --git a/packtools/sps/validation/permissions.py b/packtools/sps/validation/permissions.py new file mode 100644 index 000000000..4ba2f3505 --- /dev/null +++ b/packtools/sps/validation/permissions.py @@ -0,0 +1,363 @@ +""" +Validations for the element according to SPS 1.10 specification. + +This module implements validations for the element and its +children (, , copyright elements), which define +conditions under which content may be used, accessed, and distributed. + +Reference: https://docs.google.com/document/d/1GTv4Inc2LS_AXY-ToHT3HmO66UT0VAHWJNOIqzBNSgA/edit?tab=t.0#heading=h.permissions +""" + +import re + +from packtools.sps.validation.utils import build_response + + +XLINK_HREF = "{http://www.w3.org/1999/xlink}href" +XML_LANG = "{http://www.w3.org/XML/1998/namespace}lang" + +# Default valid CC-BY URL patterns +DEFAULT_VALID_LICENSE_URL_PATTERNS = [ + "https://creativecommons.org/licenses/by/", + "http://creativecommons.org/licenses/by/", +] + +# Default language-to-deed mapping +DEFAULT_LANG_TO_DEED = { + "pt": "deed.pt", + "en": "deed.en", + "es": "deed.es", +} + + +class PermissionsValidation: + """ + Validates the element according to SPS 1.10 rules. + + Validation rules implemented: + 1. Presence of in + 2. Uniqueness of in + 3. Presence of in + 4. Presence of @license-type="open-access" in + 5. Presence of @xlink:href in + 6. Presence of @xml:lang in + 7. Presence of in + 8. Valid CC-BY URL in @xlink:href + 9. Consistency between @xml:lang and @xlink:href + 10. Copyright structure validation + """ + + def __init__(self, xmltree, params=None): + self.xmltree = xmltree + self.params = params or {} + + # Extract article-level parent info + root = xmltree.find(".") + self._parent = { + "parent": root.tag if root is not None else None, + "parent_id": root.get("id") if root is not None else None, + "parent_article_type": root.get("article-type") if root is not None else None, + "parent_lang": root.get(XML_LANG) if root is not None else None, + } + + def validate(self): + """Run all permission validations and yield results.""" + yield from self.validate_permissions_presence() + yield from self.validate_permissions_uniqueness() + yield from self.validate_license_presence() + yield from self.validate_license_type() + yield from self.validate_xlink_href_presence() + yield from self.validate_xml_lang_presence() + yield from self.validate_license_p_presence() + yield from self.validate_license_url() + yield from self.validate_lang_link_consistency() + yield from self.validate_copyright_structure() + + def _get_permissions_nodes(self): + """Get all nodes within .""" + return self.xmltree.xpath(".//front/article-meta/permissions") + + def _get_license_nodes(self): + """Get all nodes within in .""" + return self.xmltree.xpath(".//front/article-meta/permissions/license") + + def validate_permissions_presence(self): + """Rule 1: Validate that is present in .""" + error_level = self.params.get("permissions_presence_error_level", "CRITICAL") + permissions = self._get_permissions_nodes() + is_valid = len(permissions) > 0 + + yield build_response( + title="Permissions presence", + parent=self._parent, + item="article-meta", + sub_item="permissions", + validation_type="exist", + is_valid=is_valid, + expected=" element in ", + obtained="" if is_valid else None, + advice="Add element to with a Creative Commons license declaration", + data={"permissions_count": len(permissions)}, + error_level=error_level, + ) + + def validate_permissions_uniqueness(self): + """Rule 2: Validate that appears exactly once in .""" + error_level = self.params.get("permissions_uniqueness_error_level", "ERROR") + permissions = self._get_permissions_nodes() + count = len(permissions) + + if count <= 1: + # No issue if 0 (handled by presence check) or 1 + return + + yield build_response( + title="Permissions uniqueness", + parent=self._parent, + item="article-meta", + sub_item="permissions", + validation_type="value", + is_valid=False, + expected="exactly 1 element", + obtained=f"{count} elements", + advice="Remove duplicate elements. Only one should exist in ", + data={"permissions_count": count}, + error_level=error_level, + ) + + def validate_license_presence(self): + """Rule 3: Validate that is present in .""" + error_level = self.params.get("license_presence_error_level", "CRITICAL") + permissions = self._get_permissions_nodes() + if not permissions: + return + + for perm_node in permissions: + licenses = perm_node.findall("license") + is_valid = len(licenses) > 0 + + yield build_response( + title="License presence", + parent=self._parent, + item="permissions", + sub_item="license", + validation_type="exist", + is_valid=is_valid, + expected=" element in ", + obtained="" if is_valid else None, + advice="Add element to with Creative Commons CC-BY attributes", + data={"license_count": len(licenses)}, + error_level=error_level, + ) + + def validate_license_type(self): + """Rule 4: Validate that @license-type='open-access' is present in .""" + error_level = self.params.get("license_type_error_level", "CRITICAL") + expected_type = self.params.get("expected_license_type", "open-access") + license_nodes = self._get_license_nodes() + + for license_node in license_nodes: + obtained_type = license_node.get("license-type") + is_valid = obtained_type == expected_type + + yield build_response( + title="License type", + parent=self._parent, + item="license", + sub_item="@license-type", + validation_type="value", + is_valid=is_valid, + expected=expected_type, + obtained=obtained_type, + advice=f'Set license-type="{expected_type}" in element', + data={ + "license_type": obtained_type, + "lang": license_node.get(XML_LANG), + }, + error_level=error_level, + ) + + def validate_xlink_href_presence(self): + """Rule 5: Validate that @xlink:href is present in .""" + error_level = self.params.get("xlink_href_presence_error_level", "CRITICAL") + license_nodes = self._get_license_nodes() + + for license_node in license_nodes: + href = license_node.get(XLINK_HREF) + is_valid = bool(href) + + yield build_response( + title="License xlink:href presence", + parent=self._parent, + item="license", + sub_item="@xlink:href", + validation_type="exist", + is_valid=is_valid, + expected="@xlink:href attribute in ", + obtained=href, + advice="Add xlink:href attribute with a valid Creative Commons CC-BY URL to ", + data={ + "xlink_href": href, + "lang": license_node.get(XML_LANG), + }, + error_level=error_level, + ) + + def validate_xml_lang_presence(self): + """Rule 6: Validate that @xml:lang is present in .""" + error_level = self.params.get("xml_lang_presence_error_level", "CRITICAL") + license_nodes = self._get_license_nodes() + + for license_node in license_nodes: + lang = license_node.get(XML_LANG) + is_valid = bool(lang) + + yield build_response( + title="License xml:lang presence", + parent=self._parent, + item="license", + sub_item="@xml:lang", + validation_type="exist", + is_valid=is_valid, + expected="@xml:lang attribute in ", + obtained=lang, + advice="Add xml:lang attribute to element indicating the language of the license text", + data={ + "xml_lang": lang, + "xlink_href": license_node.get(XLINK_HREF), + }, + error_level=error_level, + ) + + def validate_license_p_presence(self): + """Rule 7: Validate that is present in .""" + error_level = self.params.get("license_p_presence_error_level", "CRITICAL") + license_nodes = self._get_license_nodes() + + for license_node in license_nodes: + license_p = license_node.find("license-p") + is_valid = license_p is not None + + yield build_response( + title="License text presence", + parent=self._parent, + item="license", + sub_item="license-p", + validation_type="exist", + is_valid=is_valid, + expected=" element in ", + obtained="" if is_valid else None, + advice="Add element with the license text to ", + data={ + "has_license_p": is_valid, + "lang": license_node.get(XML_LANG), + }, + error_level=error_level, + ) + + def validate_license_url(self): + """Rule 8: Validate that @xlink:href is a valid CC-BY URL.""" + error_level = self.params.get("license_url_error_level", "ERROR") + valid_patterns = self.params.get( + "valid_license_url_patterns", DEFAULT_VALID_LICENSE_URL_PATTERNS + ) + license_nodes = self._get_license_nodes() + + for license_node in license_nodes: + href = license_node.get(XLINK_HREF) + if not href: + # Missing href is handled by validate_xlink_href_presence + continue + + is_valid = any(href.startswith(pattern) for pattern in valid_patterns) + + yield build_response( + title="License URL", + parent=self._parent, + item="license", + sub_item="@xlink:href", + validation_type="value", + is_valid=is_valid, + expected=f"a Creative Commons CC-BY URL starting with one of {valid_patterns}", + obtained=href, + advice=f"Use a valid Creative Commons CC-BY 4.0 URL, e.g. https://creativecommons.org/licenses/by/4.0/", + data={ + "xlink_href": href, + "lang": license_node.get(XML_LANG), + }, + error_level=error_level, + ) + + def validate_lang_link_consistency(self): + """Rule 9: Validate consistency between @xml:lang and @xlink:href.""" + error_level = self.params.get("lang_link_consistency_error_level", "ERROR") + lang_to_deed = self.params.get("lang_to_deed", DEFAULT_LANG_TO_DEED) + license_nodes = self._get_license_nodes() + + for license_node in license_nodes: + lang = license_node.get(XML_LANG) + href = license_node.get(XLINK_HREF) + if not lang or not href: + # Missing attributes are handled by other validations + continue + + expected_deed = lang_to_deed.get(lang) + if expected_deed is None: + # Language not in the mapping, skip consistency check + continue + + is_valid = href.endswith(expected_deed) or href.endswith(expected_deed + "/") + + yield build_response( + title="License language and URL consistency", + parent=self._parent, + item="license", + sub_item="@xml:lang and @xlink:href", + validation_type="value", + is_valid=is_valid, + expected=f"URL ending with '{expected_deed}' for language '{lang}'", + obtained=href, + advice=f"For xml:lang=\"{lang}\", use a URL ending with '{expected_deed}', " + f"e.g. https://creativecommons.org/licenses/by/4.0/{expected_deed}", + data={ + "xml_lang": lang, + "xlink_href": href, + "expected_deed": expected_deed, + }, + error_level=error_level, + ) + + def validate_copyright_structure(self): + """Rule 10: Validate copyright structure when is present.""" + error_level = self.params.get("copyright_structure_error_level", "WARNING") + permissions = self._get_permissions_nodes() + + for perm_node in permissions: + statement = perm_node.find("copyright-statement") + if statement is None: + # Copyright is optional; skip if not present + continue + + statement_text = statement.text or "" + copyright_year = perm_node.find("copyright-year") + + # Check if statement mentions a year (4 consecutive digits) + year_match = re.search(r"\b(\d{4})\b", statement_text) + if year_match and copyright_year is None: + yield build_response( + title="Copyright year", + parent=self._parent, + item="permissions", + sub_item="copyright-year", + validation_type="exist", + is_valid=False, + expected=" when year is mentioned in ", + obtained=None, + advice=f"Add {year_match.group(1)} to " + f"since the copyright statement mentions the year '{year_match.group(1)}'", + data={ + "copyright_statement": statement_text, + "mentioned_year": year_match.group(1), + }, + error_level=error_level, + ) diff --git a/packtools/sps/validation/xml_validations.py b/packtools/sps/validation/xml_validations.py index d62b5f9f9..7a55e376b 100644 --- a/packtools/sps/validation/xml_validations.py +++ b/packtools/sps/validation/xml_validations.py @@ -48,6 +48,7 @@ from packtools.sps.validation.history import HistoryValidation from packtools.sps.validation.ext_link import ExtLinkValidation from packtools.sps.validation.graphic import XMLGraphicValidation +from packtools.sps.validation.permissions import PermissionsValidation def validate_affiliations(xmltree, params): @@ -374,3 +375,21 @@ def validate_graphics(xmltree, params): graphic_rules = params["graphic_rules"] validator = XMLGraphicValidation(xmltree, graphic_rules) yield from validator.validate() + + +def validate_permissions(xmltree, params): + """ + Validates element according to SPS 1.10 specification. + + Validates: + - Presence and uniqueness of in + - Presence of with required attributes + - @license-type="open-access" + - Valid CC-BY URL in @xlink:href + - @xml:lang presence and consistency with @xlink:href + - presence + - Copyright structure when present + """ + permissions_rules = params.get("permissions_rules", {}) + validator = PermissionsValidation(xmltree, permissions_rules) + yield from validator.validate() diff --git a/packtools/sps/validation/xml_validator.py b/packtools/sps/validation/xml_validator.py index 1b6d7b311..d78f7e3ba 100644 --- a/packtools/sps/validation/xml_validator.py +++ b/packtools/sps/validation/xml_validator.py @@ -157,3 +157,7 @@ def validate_xml_content(xmltree, rules): "group": "graphic", "items": xml_validations.validate_graphics(xmltree, params), } + yield { + "group": "permissions", + "items": xml_validations.validate_permissions(xmltree, params), + } diff --git a/packtools/sps/validation_rules/permissions_rules.json b/packtools/sps/validation_rules/permissions_rules.json new file mode 100644 index 000000000..a1eeee07b --- /dev/null +++ b/packtools/sps/validation_rules/permissions_rules.json @@ -0,0 +1,24 @@ +{ + "permissions_rules": { + "permissions_presence_error_level": "CRITICAL", + "permissions_uniqueness_error_level": "ERROR", + "license_presence_error_level": "CRITICAL", + "license_type_error_level": "CRITICAL", + "xlink_href_presence_error_level": "CRITICAL", + "xml_lang_presence_error_level": "CRITICAL", + "license_p_presence_error_level": "CRITICAL", + "license_url_error_level": "ERROR", + "lang_link_consistency_error_level": "ERROR", + "copyright_structure_error_level": "WARNING", + "expected_license_type": "open-access", + "valid_license_url_patterns": [ + "https://creativecommons.org/licenses/by/", + "http://creativecommons.org/licenses/by/" + ], + "lang_to_deed": { + "pt": "deed.pt", + "en": "deed.en", + "es": "deed.es" + } + } +} diff --git a/tests/sps/validation/test_permissions.py b/tests/sps/validation/test_permissions.py new file mode 100644 index 000000000..0cae2469d --- /dev/null +++ b/tests/sps/validation/test_permissions.py @@ -0,0 +1,571 @@ +from unittest import TestCase + +from lxml import etree + +from packtools.sps.validation.permissions import PermissionsValidation + + +def _make_xmltree(permissions_xml="", article_type="research-article", lang="en"): + """Helper to build a minimal article XML tree with the given permissions block.""" + return etree.fromstring( + f"""
+ + + {permissions_xml} + + +
""" + ) + + +VALID_PERMISSIONS_EN = """ + + + This is an open-access article distributed under the terms of the Creative Commons Attribution License + + +""" + +VALID_PERMISSIONS_PT = """ + + + Este é um artigo de acesso aberto + + +""" + +VALID_PERMISSIONS_WITH_COPYRIGHT = """ + + Copyright © 2025, the authors + 2025 + the authors + + This is an open-access article + + +""" + + +class PermissionsPresenceTest(TestCase): + """Tests for Rule 1: must be present in .""" + + def test_permissions_present(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_permissions_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["title"], "Permissions presence") + + def test_permissions_missing(self): + xmltree = _make_xmltree("") + validator = PermissionsValidation(xmltree) + results = list(validator.validate_permissions_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + self.assertIsNotNone(results[0]["advice"]) + + +class PermissionsUniquenessTest(TestCase): + """Tests for Rule 2: must appear exactly once.""" + + def test_single_permissions(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_permissions_uniqueness()) + self.assertEqual(len(results), 0) # No error yielded when exactly one + + def test_duplicate_permissions(self): + xml = """ + + + text + + + + + texto + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_permissions_uniqueness()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + + def test_no_permissions(self): + xmltree = _make_xmltree("") + validator = PermissionsValidation(xmltree) + results = list(validator.validate_permissions_uniqueness()) + self.assertEqual(len(results), 0) # Handled by presence check + + +class LicensePresenceTest(TestCase): + """Tests for Rule 3: must be present in .""" + + def test_license_present(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_license_missing(self): + xml = "" + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + + def test_no_permissions(self): + xmltree = _make_xmltree("") + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_presence()) + self.assertEqual(len(results), 0) # No permissions, nothing to check + + +class LicenseTypeTest(TestCase): + """Tests for Rule 4: @license-type must be 'open-access'.""" + + def test_license_type_open_access(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_type()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + self.assertEqual(results[0]["got_value"], "open-access") + + def test_license_type_wrong(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_type()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + self.assertEqual(results[0]["got_value"], "subscription") + + def test_license_type_missing(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_type()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + self.assertIsNone(results[0]["got_value"]) + + +class XlinkHrefPresenceTest(TestCase): + """Tests for Rule 5: @xlink:href must be present in .""" + + def test_xlink_href_present(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_xlink_href_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_xlink_href_missing(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_xlink_href_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + + +class XmlLangPresenceTest(TestCase): + """Tests for Rule 6: @xml:lang must be present in .""" + + def test_xml_lang_present(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_xml_lang_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_xml_lang_missing(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_xml_lang_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + + +class LicensePPresenceTest(TestCase): + """Tests for Rule 7: must be present in .""" + + def test_license_p_present(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_p_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_license_p_missing(self): + xml = """ + + + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_p_presence()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "CRITICAL") + + +class LicenseUrlTest(TestCase): + """Tests for Rule 8: @xlink:href must be a valid CC-BY URL.""" + + def test_valid_https_url(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_url()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_valid_http_url(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_url()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_invalid_url(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_url()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + + def test_non_by_cc_url(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_url()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + + def test_missing_href_skipped(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_license_url()) + self.assertEqual(len(results), 0) # Skipped, handled by href presence check + + +class LangLinkConsistencyTest(TestCase): + """Tests for Rule 9: @xml:lang must match the language in @xlink:href.""" + + def test_consistent_en(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_lang_link_consistency()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_consistent_pt(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_PT) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_lang_link_consistency()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "OK") + + def test_inconsistent_lang_and_link(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_lang_link_consistency()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + + def test_url_without_deed_suffix(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_lang_link_consistency()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "ERROR") + + def test_missing_lang_skipped(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_lang_link_consistency()) + self.assertEqual(len(results), 0) + + def test_unknown_lang_skipped(self): + xml = """ + + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_lang_link_consistency()) + self.assertEqual(len(results), 0) + + +class CopyrightStructureTest(TestCase): + """Tests for Rule 10: Validate copyright structure when present.""" + + def test_copyright_with_year_element(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_WITH_COPYRIGHT) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_copyright_structure()) + self.assertEqual(len(results), 0) # No warning, structure is complete + + def test_copyright_statement_mentions_year_but_missing_element(self): + xml = """ + + Copyright © 2025, the authors + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_copyright_structure()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["response"], "WARNING") + self.assertIn("2025", results[0]["advice"]) + + def test_copyright_statement_without_year_no_warning(self): + xml = """ + + Copyright the authors + + text + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_copyright_structure()) + self.assertEqual(len(results), 0) + + def test_no_copyright_no_warning(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate_copyright_structure()) + self.assertEqual(len(results), 0) + + +class FullValidateTest(TestCase): + """Tests for the full validate() method with valid and invalid XML.""" + + def test_valid_xml_all_pass(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + validator = PermissionsValidation(xmltree) + results = list(validator.validate()) + for result in results: + self.assertEqual( + result["response"], + "OK", + f"Validation '{result['title']}' failed: {result.get('advice')}", + ) + + def test_valid_xml_with_copyright_all_pass(self): + xmltree = _make_xmltree(VALID_PERMISSIONS_WITH_COPYRIGHT) + validator = PermissionsValidation(xmltree) + results = list(validator.validate()) + for result in results: + self.assertEqual( + result["response"], + "OK", + f"Validation '{result['title']}' failed: {result.get('advice')}", + ) + + def test_empty_article_meta_yields_critical(self): + xmltree = _make_xmltree("") + validator = PermissionsValidation(xmltree) + results = list(validator.validate()) + # Should have at least the permissions presence check + self.assertTrue(len(results) >= 1) + self.assertEqual(results[0]["response"], "CRITICAL") + self.assertEqual(results[0]["title"], "Permissions presence") + + def test_multiple_licenses(self): + xml = """ + + + English license text + + + Texto de licença em português + + + """ + xmltree = _make_xmltree(xml) + validator = PermissionsValidation(xmltree) + results = list(validator.validate()) + for result in results: + self.assertEqual( + result["response"], + "OK", + f"Validation '{result['title']}' failed: {result.get('advice')}", + ) + + def test_custom_error_levels(self): + xmltree = _make_xmltree("") + params = {"permissions_presence_error_level": "WARNING"} + validator = PermissionsValidation(xmltree, params) + results = list(validator.validate_permissions_presence()) + self.assertEqual(results[0]["response"], "WARNING") + + +class ResponseStructureTest(TestCase): + """Tests verifying that response dictionaries have correct structure.""" + + def setUp(self): + self.xmltree = _make_xmltree(VALID_PERMISSIONS_EN) + self.validator = PermissionsValidation(self.xmltree) + self.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", + } + + def test_response_has_expected_keys(self): + results = list(self.validator.validate()) + self.assertTrue(len(results) > 0) + for result in results: + self.assertEqual(set(result.keys()), self.expected_keys) + + def test_parent_info(self): + results = list(self.validator.validate()) + for result in results: + self.assertEqual(result["parent"], "article") + self.assertEqual(result["parent_article_type"], "research-article") + self.assertEqual(result["parent_lang"], "en")