From 7d8e32ba06ba78965fcda909672637f0a9b33afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Thu, 25 Apr 2019 10:56:34 -0400 Subject: [PATCH] dte.data_models: modify new fields of `DteDataL2` Modify 2 fields added to `DteDataL2` in 89196d3c (PR #21). That introduces some changes to function `parse_dte_xml`. Also includes a major reorganization of tests for module `parse`, improving separate coverage. --- cl_sii/dte/data_models.py | 50 +++-- cl_sii/dte/parse.py | 20 +- tests/test_dte_data_models.py | 8 +- tests/test_dte_parse.py | 362 +++++++++++++++++++--------------- 4 files changed, 256 insertions(+), 184 deletions(-) diff --git a/cl_sii/dte/data_models.py b/cl_sii/dte/data_models.py index d7e43653..4591cda5 100644 --- a/cl_sii/dte/data_models.py +++ b/cl_sii/dte/data_models.py @@ -22,6 +22,7 @@ import cl_sii.contribuyente.constants import cl_sii.rut.constants +from cl_sii.libs import encoding_utils from cl_sii.rut import Rut from . import constants @@ -79,9 +80,19 @@ def validate_clean_str(value: str) -> None: raise ValueError("Value has leading or trailing whitespace characters.", value) +def validate_clean_bytes(value: bytes) -> None: + if len(value.strip()) != len(value): + raise ValueError("Value has leading or trailing whitespace characters.", value) + + def validate_non_empty_str(value: str) -> None: if len(value.strip()) == 0: - raise ValueError("String (stripped) length is 0.") + raise ValueError("String value length (stripped) is 0.") + + +def validate_non_empty_bytes(value: bytes) -> None: + if len(value.strip()) == 0: + raise ValueError("Bytes value length (stripped) is 0.") @dataclasses.dataclass(frozen=True) @@ -327,14 +338,16 @@ class DteDataL2(DteDataL1): Datetime on which the "documento" was digitally signed. """ - signature_value_base64: Optional[str] = dc_field(default=None) + signature_value: Optional[bytes] = dc_field(default=None) """ - DTE's digital signature's value (as base64 str). + DTE's digital signature's value (raw bytes, without base64 encoding). """ - signature_x509_cert_base64: Optional[str] = dc_field(default=None) + signature_x509_cert_pem: Optional[bytes] = dc_field(default=None) """ - DTE's digital signature's X509 certificate (as base64 str). + DTE's digital signature's PEM-encoded X.509 cert. + + PEM-encoded implies base64-encoded. """ emisor_giro: Optional[str] = dc_field(default=None) @@ -377,21 +390,18 @@ def __post_init__(self) -> None: if not isinstance(self.firma_documento_dt_naive, datetime): raise TypeError("Inappropriate type of 'firma_documento_dt_naive'.") - if self.signature_value_base64 is not None: - if not isinstance(self.signature_value_base64, str): - raise TypeError("Inappropriate type of 'signature_value_base64'.") - # TODO: validate that it is base64 - # TODO: bytes? - validate_clean_str(self.signature_value_base64) - validate_non_empty_str(self.signature_value_base64) - - if self.signature_x509_cert_base64 is not None: - if not isinstance(self.signature_x509_cert_base64, str): - raise TypeError("Inappropriate type of 'signature_x509_cert_base64'.") - # TODO: validate that it is base64 - # TODO: bytes? - validate_clean_str(self.signature_x509_cert_base64) - validate_non_empty_str(self.signature_x509_cert_base64) + if self.signature_value is not None: + if not isinstance(self.signature_value, bytes): + raise TypeError("Inappropriate type of 'signature_value'.") + validate_clean_bytes(self.signature_value) + validate_non_empty_bytes(self.signature_value) + + if self.signature_x509_cert_pem is not None: + if not isinstance(self.signature_x509_cert_pem, bytes): + raise TypeError("Inappropriate type of 'signature_x509_cert_pem'.") + validate_clean_bytes(self.signature_x509_cert_pem) + validate_non_empty_bytes(self.signature_x509_cert_pem) + encoding_utils.validate_base64(self.signature_x509_cert_pem) if self.emisor_giro is not None: if not isinstance(self.emisor_giro, str): diff --git a/cl_sii/dte/parse.py b/cl_sii/dte/parse.py index c8956ba4..5cd5831f 100644 --- a/cl_sii/dte/parse.py +++ b/cl_sii/dte/parse.py @@ -23,8 +23,9 @@ from datetime import date, datetime from typing import Tuple +from cl_sii.libs import encoding_utils from cl_sii.libs import xml_utils -from cl_sii.libs.xml_utils import XmlElement +from cl_sii.libs.xml_utils import XmlElement, XmlElementTree from cl_sii.rut import Rut from . import constants from . import data_models @@ -121,12 +122,19 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: It is assumed that ``xml_doc`` is an ``{http://www.sii.cl/SiiDte}/DTE`` XML element. + :raises ValueError: + :raises TypeError: + :raises NotImplementedError: + """ # TODO: change response type to a dataclass like 'DteXmlData'. # TODO: separate the XML parsing stage from the deserialization stage, which could be # performed by XML-agnostic code (perhaps using Marshmallow or data clacases?). # See :class:`cl_sii.rcv.parse.RcvCsvRowSchema`. + if not isinstance(xml_doc, (XmlElement, XmlElementTree)): + raise TypeError("'xml_doc' must be an 'XmlElement'.") + xml_em = xml_doc ########################################################################### @@ -441,8 +449,10 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: tmst_firma_value = datetime.fromisoformat(tmst_firma_em.text) - signature_signature_value_base64 = signature_signature_value_em.text.strip() - signature_key_info_x509_cert_base64 = signature_key_info_x509_cert_em.text.strip() + signature_signature_value = encoding_utils.decode_base64_strict( + signature_signature_value_em.text.strip()) + signature_key_info_x509_cert_pem = encoding_utils.clean_base64( + signature_key_info_x509_cert_em.text.strip()) return data_models.DteDataL2( emisor_rut=emisor_rut_value, @@ -455,8 +465,8 @@ def parse_dte_xml(xml_doc: XmlElement) -> data_models.DteDataL2: receptor_razon_social=receptor_razon_social_value, fecha_vencimiento_date=fecha_vencimiento_value, firma_documento_dt_naive=tmst_firma_value, - signature_value_base64=signature_signature_value_base64, - signature_x509_cert_base64=signature_key_info_x509_cert_base64, + signature_value=signature_signature_value, + signature_x509_cert_pem=signature_key_info_x509_cert_pem, emisor_giro=emisor_giro_value, emisor_email=emisor_email_value, receptor_email=receptor_email_value, diff --git a/tests/test_dte_data_models.py b/tests/test_dte_data_models.py index 45040d98..d0d4acad 100644 --- a/tests/test_dte_data_models.py +++ b/tests/test_dte_data_models.py @@ -151,8 +151,8 @@ def setUp(self) -> None: receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), - signature_value_base64=None, - signature_x509_cert_base64=None, + signature_value=None, + signature_x509_cert_pem=None, emisor_giro='Ingenieria y Construccion', emisor_email='hello@example.com', receptor_email=None, @@ -176,8 +176,8 @@ def test_as_dict(self) -> None: receptor_razon_social='MINERA LOS PELAMBRES', fecha_vencimiento_date=None, firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), - signature_value_base64=None, - signature_x509_cert_base64=None, + signature_value=None, + signature_x509_cert_pem=None, emisor_giro='Ingenieria y Construccion', emisor_email='hello@example.com', receptor_email=None, diff --git a/tests/test_dte_parse.py b/tests/test_dte_parse.py index 85c4de83..1944f17d 100644 --- a/tests/test_dte_parse.py +++ b/tests/test_dte_parse.py @@ -4,6 +4,8 @@ from datetime import date, datetime import cl_sii.dte.constants +from cl_sii.libs import crypto_utils +from cl_sii.libs import encoding_utils from cl_sii.libs import xml_utils from cl_sii.rut import Rut @@ -16,92 +18,95 @@ from .utils import read_test_file_bytes -_TEST_DTE_NEEDS_CLEAN_1_FILE_PATH = 'test_data/sii-dte/DTE--76354771-K--33--170.xml' -_TEST_DTE_NEEDS_CLEAN_2_FILE_PATH = 'test_data/sii-dte/DTE--76399752-9--33--25568.xml' -_TEST_DTE_1_FILE_PATH = _TEST_DTE_NEEDS_CLEAN_1_FILE_PATH -_TEST_DTE_2_FILE_PATH = _TEST_DTE_NEEDS_CLEAN_2_FILE_PATH -_TEST_DTE_1_SIGNATURE_VALUE = ( - 'fsYP5p/lNfofAz8POShrJjqXdBTNNtvv4/TWCxbvwTIAXr7BLrlvX3C/Hpfo4viqaxSu1OGFgPnk\n' - 'ddDIFwj/ZsVdbdB+MhpKkyha83RxhJpYBVBY3c+y9J6oMfdIdMAYXhEkFw8w63KHyhdf2E9dnbKi\n' - 'wqSxDcYjTT6vXsLPrZk=') -_TEST_DTE_2_SIGNATURE_VALUE = ( - 'wwOMQuFqa6c5gzYSJ5PWfo0OiAf+yNcJK6wx4xJ3VNehlAcMrUB2q+rK/DDhCvjxAoX4NxBACiFD\n' - 'MrTMIfvxrwXjLd1oX37lSFOtsWX6JxL0SV+tLF7qvWCu1Yzw8ypUf7GDkbymJkoTYDF9JFF8kYU4\n' - 'FdU2wttiwne9XH8QFHgXsocKP/aygwiOeGqiNX9o/O5XS2GWpt+KM20jrvtYn7UFMED/3aPacCb1\n' - 'GABizr8mlVEZggZgJunMDChpFQyEigSXMK5I737Ac8D2bw7WB47Wj1WBL3sCFRDlXUXtnMvChBVp\n' - '0HRUXYuKHyfpCzqIBXygYrIZexxXgOSnKu/yGg==') -_TEST_DTE_1_X509_CERT = ( - 'MIIGVDCCBTygAwIBAgIKMUWmvgAAAAjUHTANBgkqhkiG9w0BAQUFADCB0jELMAkGA1UEBhMCQ0wx\n' - 'HTAbBgNVBAgTFFJlZ2lvbiBNZXRyb3BvbGl0YW5hMREwDwYDVQQHEwhTYW50aWFnbzEUMBIGA1UE\n' - 'ChMLRS1DRVJUQ0hJTEUxIDAeBgNVBAsTF0F1dG9yaWRhZCBDZXJ0aWZpY2Fkb3JhMTAwLgYDVQQD\n' - 'EydFLUNFUlRDSElMRSBDQSBGSVJNQSBFTEVDVFJPTklDQSBTSU1QTEUxJzAlBgkqhkiG9w0BCQEW\n' - 'GHNjbGllbnRlc0BlLWNlcnRjaGlsZS5jbDAeFw0xNzA5MDQyMTExMTJaFw0yMDA5MDMyMTExMTJa\n' - 'MIHXMQswCQYDVQQGEwJDTDEUMBIGA1UECBMLVkFMUEFSQUlTTyAxETAPBgNVBAcTCFF1aWxsb3Rh\n' - 'MS8wLQYDVQQKEyZTZXJ2aWNpb3MgQm9uaWxsYSB5IExvcGV6IHkgQ2lhLiBMdGRhLjEkMCIGA1UE\n' - 'CwwbSW5nZW5pZXLDrWEgeSBDb25zdHJ1Y2Npw7NuMSMwIQYDVQQDExpSYW1vbiBodW1iZXJ0byBM\n' - 'b3BleiAgSmFyYTEjMCEGCSqGSIb3DQEJARYUZW5hY29ubHRkYUBnbWFpbC5jb20wgZ8wDQYJKoZI\n' - 'hvcNAQEBBQADgY0AMIGJAoGBAKQeAbNDqfi9M2v86RUGAYgq1ZSDioFC6OLr0SwiOaYnLsSOl+Kx\n' - 'O394PVwSGa6rZk1ErIZonyi15fU/0nHZLi8iHLB49EB5G3tCwh0s8NfqR9ck0/3Z+TXhVUdiJyJC\n' - '/z8x5I5lSUfzNEedJRidVvp6jVGr7P/SfoEfQQTLP3mBAgMBAAGjggKnMIICozA9BgkrBgEEAYI3\n' - 'FQcEMDAuBiYrBgEEAYI3FQiC3IMvhZOMZoXVnReC4twnge/sPGGBy54UhqiCWAIBZAIBBDAdBgNV\n' - 'HQ4EFgQU1dVHhF0UVe7RXIz4cjl3/Vew+qowCwYDVR0PBAQDAgTwMB8GA1UdIwQYMBaAFHjhPp/S\n' - 'ErN6PI3NMA5Ts0MpB7NVMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dHA6Ly9jcmwuZS1jZXJ0Y2hpbGUu\n' - 'Y2wvZWNlcnRjaGlsZWNhRkVTLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0dHA6\n' - 'Ly9vY3NwLmVjZXJ0Y2hpbGUuY2wvb2NzcDAjBgNVHREEHDAaoBgGCCsGAQQBwQEBoAwWCjEzMTg1\n' - 'MDk1LTYwIwYDVR0SBBwwGqAYBggrBgEEAcEBAqAMFgo5NjkyODE4MC01MIIBTQYDVR0gBIIBRDCC\n' - 'AUAwggE8BggrBgEEAcNSBTCCAS4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cuZS1jZXJ0Y2hpbGUu\n' - 'Y2wvQ1BTLmh0bTCB/AYIKwYBBQUHAgIwge8egewAQwBlAHIAdABpAGYAaQBjAGEAZABvACAARgBp\n' - 'AHIAbQBhACAAUwBpAG0AcABsAGUALgAgAEgAYQAgAHMAaQBkAG8AIAB2AGEAbABpAGQAYQBkAG8A\n' - 'IABlAG4AIABmAG8AcgBtAGEAIABwAHIAZQBzAGUAbgBjAGkAYQBsACwAIABxAHUAZQBkAGEAbgBk\n' - 'AG8AIABoAGEAYgBpAGwAaQB0AGEAZABvACAAZQBsACAAQwBlAHIAdABpAGYAaQBjAGEAZABvACAA\n' - 'cABhAHIAYQAgAHUAcwBvACAAdAByAGkAYgB1AHQAYQByAGkAbzANBgkqhkiG9w0BAQUFAAOCAQEA\n' - 'mxtPpXWslwI0+uJbyuS9s/S3/Vs0imn758xMU8t4BHUd+OlMdNAMQI1G2+q/OugdLQ/a9Sg3clKD\n' - 'qXR4lHGl8d/Yq4yoJzDD3Ceez8qenY3JwGUhPzw9oDpg4mXWvxQDXSFeW/u/BgdadhfGnpwx61Un\n' - '+/fU24ZgU1dDJ4GKj5oIPHUIjmoSBhnstEhIr6GJWSTcDKTyzRdqBlaVhenH2Qs6Mw6FrOvRPuud\n' - 'B7lo1+OgxMb/Gjyu6XnEaPu7Vq4XlLYMoCD2xrV7WEADaDTm7KcNLczVAYqWSF1WUqYSxmPoQDFY\n' - '+kMTThJyCXBlE0NADInrkwWgLLygkKI7zXkwaw==') -_TEST_DTE_2_X509_CERT = ( - 'MIIF/zCCBOegAwIBAgICMhQwDQYJKoZIhvcNAQELBQAwgaYxCzAJBgNVBAYTAkNMMRgwFgYDVQQK\n' - 'Ew9BY2VwdGEuY29tIFMuQS4xSDBGBgNVBAMTP0FjZXB0YS5jb20gQXV0b3JpZGFkIENlcnRpZmlj\n' - 'YWRvcmEgQ2xhc2UgMiBQZXJzb25hIE5hdHVyYWwgLSBHNDEeMBwGCSqGSIb3DQEJARYPaW5mb0Bh\n' - 'Y2VwdGEuY29tMRMwEQYDVQQFEwo5NjkxOTA1MC04MB4XDTE3MDEwNjE0MDI1NFoXDTIwMDEwNjE0\n' - 'MDI1NFowgY8xCzAJBgNVBAYTAkNMMRgwFgYDVQQMEw9QRVJTT05BIE5BVFVSQUwxIzAhBgNVBAMT\n' - 'GkdJQU5JTkEgQkVMRU4gRElBWiBVUlJVVElBMSwwKgYJKoZIhvcNAQkBFh1kYW5pZWwuYXJhdmVu\n' - 'YUBpbm5vdmFtb2JlbC5jbDETMBEGA1UEBRMKMTY0Nzc3NTItOTCCASIwDQYJKoZIhvcNAQEBBQAD\n' - 'ggEPADCCAQoCggEBANLQYWfXROtuPiyInyROQc+DZ2LdpvaShxU6iU2xB+CQs74HZ+oS1BINzmL1\n' - 'g9oY7hHvT+/H+hucOlN7xomH/UuDikjoySjhbH3xBMzh6qWHvDqcfTswYuHES2hO9keTzwytyUIP\n' - 'HTctMNJ32mIQ/fGU8H+Qf7adtV+A7k3jXgvCu3DQ5ceeR1xUyDbTXIWJDtg215sa3YSkto3iPNSh\n' - 'qiKeGfsh/qUEaH3oK/Tf0lOG/CG/bnvLdubacc9o7B5QS6JF5ILMffCEuzBrxyMZLhBQYm1ah6dS\n' - 'EbCsDNkc6sQMHLYg/0qG1N+cILXVyusGGCCEDTfmXb/AI4rEKaJt0XMCAwEAAaOCAkowggJGMB8G\n' - 'A1UdIwQYMBaAFGWlqz4/yLZRbRF+X8MKB+ZDoAi2MB0GA1UdDgQWBBSHoSD4nd2UJuwzmJnJud0L\n' - 'WSO+MzALBgNVHQ8EBAMCBPAwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMBEGCWCGSAGG\n' - '+EIBAQQEAwIFoDB1BgNVHSAEbjBsMGoGCCsGAQQBtWsCMF4wMQYIKwYBBQUHAgEWJWh0dHBzOi8v\n' - 'YWNnNC5hY2VwdGEuY29tL0NQUy1BY2VwdGFjb20wKQYIKwYBBQUHAgIwHTAWFg9BY2VwdGEuY29t\n' - 'IFMuQS4wAwIBCRoDVEJEMFoGA1UdEgRTMFGgGAYIKwYBBAHBAQKgDBYKOTY5MTkwNTAtOKAkBggr\n' - 'BgEFBQcIA6AYMBYMCjk2OTE5MDUwLTgGCCsGAQQBwQECgQ9pbmZvQGFjZXB0YS5jb20waAYDVR0R\n' - 'BGEwX6AYBggrBgEEAcEBAaAMFgoxNjQ3Nzc1Mi05oCQGCCsGAQUFBwgDoBgwFgwKMTY0Nzc3NTIt\n' - 'OQYIKwYBBAHBAQKBHWRhbmllbC5hcmF2ZW5hQGlubm92YW1vYmVsLmNsMEcGCCsGAQUFBwEBBDsw\n' - 'OTA3BggrBgEFBQcwAYYraHR0cHM6Ly9hY2c0LmFjZXB0YS5jb20vYWNnNC9vY3NwL0NsYXNlMi1H\n' - 'NDA/BgNVHR8EODA2MDSgMqAwhi5odHRwczovL2FjZzQuYWNlcHRhLmNvbS9hY2c0L2NybC9DbGFz\n' - 'ZTItRzQuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQCx+mdIdIu1QQf6mnFDCYfcyhU5t5iKV+8Pr8LV\n' - 'WZdlwGmKRbzhqYKZ8oo5Bfmto105z7JYJIFyZiny/8sb9IcoPLNG/6LtWZZFmHkZabC9sUEjSxU/\n' - 'w8w2VMhrCILonVjnhLX8VHNMkc3Xy17JgvUAIcor2MHfNxn0lyEM3EZdROkgDxwuWfS388mqg8KB\n' - 'B/QNi7AB5U9kB7M5wfGr2lYAvkzlTmHlcBFI2fI6odZlfzLnyKN/ow9mow4Z4ngKuhlTpTUVrACg\n' - 'jhl1gijANMhS1SwNpPgOLlf54KbXTQxWrrwt9mEMZBH7w6imtxJGzNWPjPcykRB7YQxhrHkfzmrw') - - class OthersTest(unittest.TestCase): def test_DTE_XML_SCHEMA_OBJ(self) -> None: # TODO: implement pass - def test_integration_ok_1(self) -> None: - # TODO: split in separate tests, with more coverage. - dte_bad_xml_file_path = _TEST_DTE_NEEDS_CLEAN_1_FILE_PATH +class FunctionValidateDteXmlTest(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.dte_bad_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170.xml') + cls.dte_bad_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568.xml') + + cls.dte_clean_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml') + cls.dte_clean_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml') + + def test_validate_dte_xml_ok_dte_1(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_1_xml_bytes) + validate_dte_xml(xml_doc) + + self.assertEqual( + xml_doc.getroottree().getroot().tag, + '{%s}DTE' % DTE_XMLNS) + + def test_validate_dte_xml_ok_dte_2(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_2_xml_bytes) + validate_dte_xml(xml_doc) + + self.assertEqual( + xml_doc.getroottree().getroot().tag, + '{%s}DTE' % DTE_XMLNS) + + def test_validate_dte_xml_fail_x(self) -> None: + # TODO: implement more cases + pass + + def test_validate_dte_xml_fail_dte_1(self) -> None: + file_bytes = self.dte_bad_xml_1_xml_bytes + xml_doc = xml_utils.parse_untrusted_xml(file_bytes) + + self.assertEqual( + xml_doc.getroottree().getroot().tag, + 'DTE') - file_bytes = read_test_file_bytes(dte_bad_xml_file_path) + with self.assertRaises(xml_utils.XmlSchemaDocValidationError) as cm: + validate_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Element 'DTE': No matching global declaration available for the validation root., " + "line 2", ) + ) + + def test_validate_dte_xml_fail_dte_2(self) -> None: + file_bytes = self.dte_bad_xml_2_xml_bytes + xml_doc = xml_utils.parse_untrusted_xml(file_bytes) + + self.assertEqual( + xml_doc.getroottree().getroot().tag, + 'DTE') + + with self.assertRaises(xml_utils.XmlSchemaDocValidationError) as cm: + validate_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Element 'DTE': No matching global declaration available for the validation root., " + "line 2", ) + ) + + +class FunctionCleanDteXmlTest(unittest.TestCase): + + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.dte_bad_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170.xml') + cls.dte_bad_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568.xml') + + def test_clean_dte_xml_ok_1(self) -> None: + file_bytes = self.dte_bad_xml_1_xml_bytes xml_doc = xml_utils.parse_untrusted_xml(file_bytes) self.assertEqual( @@ -115,8 +120,6 @@ def test_integration_ok_1(self) -> None: ("Element 'DTE': No matching global declaration available for the validation root., " "line 2", ) ) - # This would raise: - # parse_dte_xml(xml_doc) xml_doc_cleaned, modified = clean_dte_xml( xml_doc, @@ -128,10 +131,6 @@ def test_integration_ok_1(self) -> None: # This will not raise. validate_dte_xml(xml_doc_cleaned) - self.assertEqual( - xml_doc_cleaned.getroottree().getroot().tag, - '{%s}DTE' % DTE_XMLNS) - f = io.BytesIO() xml_utils.write_xml_doc(xml_doc_cleaned, f) file_bytes_rewritten = f.getvalue() @@ -139,27 +138,6 @@ def test_integration_ok_1(self) -> None: xml_doc_rewritten = xml_utils.parse_untrusted_xml(file_bytes_rewritten) validate_dte_xml(xml_doc_rewritten) - parsed_dte_rewritten = parse_dte_xml(xml_doc_cleaned) - - self.assertDictEqual( - dict(parsed_dte_rewritten.as_dict()), - dict( - emisor_rut=Rut('76354771-K'), - tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, - folio=170, - fecha_emision_date=date(2019, 4, 1), - receptor_rut=Rut('96790240-3'), - monto_total=2996301, - emisor_razon_social='INGENIERIA ENACON SPA', - receptor_razon_social='MINERA LOS PELAMBRES', - fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), - signature_value_base64=_TEST_DTE_1_SIGNATURE_VALUE, - signature_x509_cert_base64=_TEST_DTE_1_X509_CERT, - emisor_giro='Ingenieria y Construccion', - emisor_email='ENACONLTDA@GMAIL.COM', - receptor_email=None, - )) expected_file_bytes_diff = ( b'--- \n', @@ -201,12 +179,8 @@ def test_integration_ok_1(self) -> None: expected_file_bytes_diff ) - def test_integration_ok_2(self) -> None: - # TODO: split in separate tests, with more coverage. - - dte_bad_xml_file_path = _TEST_DTE_NEEDS_CLEAN_2_FILE_PATH - - file_bytes = read_test_file_bytes(dte_bad_xml_file_path) + def test_clean_dte_xml_ok_2(self) -> None: + file_bytes = self.dte_bad_xml_2_xml_bytes xml_doc = xml_utils.parse_untrusted_xml(file_bytes) self.assertEqual( @@ -220,8 +194,6 @@ def test_integration_ok_2(self) -> None: ("Element 'DTE': No matching global declaration available for the validation root., " "line 2", ) ) - # This would raise: - # parse_dte_xml(xml_doc) xml_doc_cleaned, modified = clean_dte_xml( xml_doc, @@ -233,10 +205,6 @@ def test_integration_ok_2(self) -> None: # This will not raise. validate_dte_xml(xml_doc_cleaned) - self.assertEqual( - xml_doc_cleaned.getroottree().getroot().tag, - '{%s}DTE' % DTE_XMLNS) - f = io.BytesIO() xml_utils.write_xml_doc(xml_doc_cleaned, f) file_bytes_rewritten = f.getvalue() @@ -244,27 +212,6 @@ def test_integration_ok_2(self) -> None: xml_doc_rewritten = xml_utils.parse_untrusted_xml(file_bytes_rewritten) validate_dte_xml(xml_doc_rewritten) - parsed_dte_rewritten = parse_dte_xml(xml_doc_cleaned) - - self.assertDictEqual( - dict(parsed_dte_rewritten.as_dict()), - dict( - emisor_rut=Rut('76399752-9'), - tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, - folio=25568, - fecha_emision_date=date(2019, 3, 29), - receptor_rut=Rut('96874030-K'), - monto_total=230992, - emisor_razon_social='COMERCIALIZADORA INNOVA MOBEL SPA', - receptor_razon_social='EMPRESAS LA POLAR S.A.', - fecha_vencimiento_date=None, - firma_documento_dt_naive=datetime(2019, 3, 28, 13, 59, 52), - signature_value_base64=_TEST_DTE_2_SIGNATURE_VALUE, - signature_x509_cert_base64=_TEST_DTE_2_X509_CERT, - emisor_giro='COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR', - emisor_email='ANGEL.PEZO@APCASESORIAS.CL', - receptor_email=None, - )) expected_file_bytes_diff = ( b'--- \n', @@ -306,41 +253,146 @@ def test_integration_ok_2(self) -> None: expected_file_bytes_diff ) - -class FunctionCleanDteXmlTest(unittest.TestCase): - - def test_clean_dte_xml_ok(self) -> None: - # TODO: implement - pass - def test_clean_dte_xml_fail(self) -> None: - # TODO: implement + # TODO: implement for 'clean_dte_xml', for many cases. pass def test__set_dte_xml_missing_xmlns_ok(self) -> None: - # TODO: implement + # TODO: implement for '_set_dte_xml_missing_xmlns'. pass def test__set_dte_xml_missing_xmlns_fail(self) -> None: - # TODO: implement + # TODO: implement for '_set_dte_xml_missing_xmlns'. pass def test__remove_dte_xml_doc_personalizado_ok(self) -> None: - # TODO: implement + # TODO: implement for '_remove_dte_xml_doc_personalizado'. pass def test__remove_dte_xml_doc_personalizado_fail(self) -> None: - # TODO: implement + # TODO: implement for '_remove_dte_xml_doc_personalizado'. pass class FunctionParseDteXmlTest(unittest.TestCase): - # TODO: implement - pass + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + + cls.dte_bad_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170.xml') + cls.dte_bad_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568.xml') + + cls.dte_clean_xml_1_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76354771-K--33--170--cleaned.xml') + cls.dte_clean_xml_2_xml_bytes = read_test_file_bytes( + 'test_data/sii-dte/DTE--76399752-9--33--25568--cleaned.xml') + + cls.dte_clean_xml_1_cert_pem_bytes = encoding_utils.clean_base64( + crypto_utils.remove_pem_cert_header_footer( + read_test_file_bytes('test_data/sii-crypto/DTE--76354771-K--33--170-cert.pem'))) + cls.dte_clean_xml_2_cert_pem_bytes = encoding_utils.clean_base64( + crypto_utils.remove_pem_cert_header_footer( + read_test_file_bytes('test_data/sii-crypto/DTE--76399752-9--33--25568-cert.pem'))) + + cls._TEST_DTE_1_SIGNATURE_VALUE = encoding_utils.decode_base64_strict( + read_test_file_bytes( + 'test_data/sii-crypto/DTE--76354771-K--33--170-signature-value-base64.txt')) + cls._TEST_DTE_2_SIGNATURE_VALUE = encoding_utils.decode_base64_strict( + read_test_file_bytes( + 'test_data/sii-crypto/DTE--76399752-9--33--25568-signature-value-base64.txt')) + + def test_data(self): + self.assertEqual( + self._TEST_DTE_1_SIGNATURE_VALUE, + b'~\xc6\x0f\xe6\x9f\xe55\xfa\x1f\x03?\x0f9(k&:\x97t\x14\xcd6\xdb\xef\xe3\xf4\xd6' + b'\x0b\x16\xef\xc12\x00^\xbe\xc1.\xb9o_p\xbf\x1e\x97\xe8\xe2\xf8\xaak\x14\xae\xd4' + b'\xe1\x85\x80\xf9\xe4u\xd0\xc8\x17\x08\xfff\xc5]m\xd0~2\x1aJ\x93(Z\xf3tq\x84\x9a' + b'X\x05PX\xdd\xcf\xb2\xf4\x9e\xa81\xf7Ht\xc0\x18^\x11$\x17\x0f0\xebr\x87\xca\x17_' + b'\xd8O]\x9d\xb2\xa2\xc2\xa4\xb1\r\xc6#M>\xaf^\xc2\xcf\xad\x99') + self.assertEqual( + self._TEST_DTE_2_SIGNATURE_VALUE, + b"\xc3\x03\x8cB\xe1jk\xa79\x836\x12'\x93\xd6~\x8d\x0e\x88\x07\xfe\xc8\xd7\t+\xac1" + b"\xe3\x12wT\xd7\xa1\x94\x07\x0c\xad@v\xab\xea\xca\xfc0\xe1\n\xf8\xf1\x02\x85\xf87" + b"\x10@\n!C2\xb4\xcc!\xfb\xf1\xaf\x05\xe3-\xddh_~\xe5HS\xad\xb1e\xfa'\x12\xf4I_" + b"\xad,^\xea\xbd`\xae\xd5\x8c\xf0\xf3*T\x7f\xb1\x83\x91\xbc\xa6&J\x13`1}$Q|\x91" + b"\x858\x15\xd56\xc2\xdbb\xc2w\xbd\\\x7f\x10\x14x\x17\xb2\x87\n?\xf6\xb2\x83\x08" + b"\x8exj\xa25\x7fh\xfc\xeeWKa\x96\xa6\xdf\x8a3m#\xae\xfbX\x9f\xb5\x050@\xff\xdd" + b"\xa3\xdap&\xf5\x18\x00b\xce\xbf&\x95Q\x19\x82\x06`&\xe9\xcc\x0c(i\x15\x0c\x84" + b"\x8a\x04\x970\xaeH\xef~\xc0s\xc0\xf6o\x0e\xd6\x07\x8e\xd6\x8fU\x81/{\x02\x15\x10" + b"\xe5]E\xed\x9c\xcb\xc2\x84\x15i\xd0tT]\x8b\x8a\x1f'\xe9\x0b:\x88\x05|\xa0b\xb2" + b"\x19{\x1cW\x80\xe4\xa7*\xef\xf2\x1a") + + def test_parse_dte_xml_ok_1(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_1_xml_bytes) + + parsed_dte = parse_dte_xml(xml_doc) + self.assertDictEqual( + dict(parsed_dte.as_dict()), + dict( + emisor_rut=Rut('76354771-K'), + tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, + folio=170, + fecha_emision_date=date(2019, 4, 1), + receptor_rut=Rut('96790240-3'), + monto_total=2996301, + emisor_razon_social='INGENIERIA ENACON SPA', + receptor_razon_social='MINERA LOS PELAMBRES', + fecha_vencimiento_date=None, + firma_documento_dt_naive=datetime(2019, 4, 1, 1, 36, 40), + signature_value=self._TEST_DTE_1_SIGNATURE_VALUE, + signature_x509_cert_pem=self.dte_clean_xml_1_cert_pem_bytes, + emisor_giro='Ingenieria y Construccion', + emisor_email='ENACONLTDA@GMAIL.COM', + receptor_email=None, + )) + def test_parse_dte_xml_ok_2(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_clean_xml_2_xml_bytes) -class FunctionValidateDteXmlTest(unittest.TestCase): + parsed_dte = parse_dte_xml(xml_doc) + self.assertDictEqual( + dict(parsed_dte.as_dict()), + dict( + emisor_rut=Rut('76399752-9'), + tipo_dte=cl_sii.dte.constants.TipoDteEnum.FACTURA_ELECTRONICA, + folio=25568, + fecha_emision_date=date(2019, 3, 29), + receptor_rut=Rut('96874030-K'), + monto_total=230992, + emisor_razon_social='COMERCIALIZADORA INNOVA MOBEL SPA', + receptor_razon_social='EMPRESAS LA POLAR S.A.', + fecha_vencimiento_date=None, + firma_documento_dt_naive=datetime(2019, 3, 28, 13, 59, 52), + signature_value=self._TEST_DTE_2_SIGNATURE_VALUE, + signature_x509_cert_pem=self.dte_clean_xml_2_cert_pem_bytes, + emisor_giro='COMERCIALIZACION DE PRODUCTOS PARA EL HOGAR', + emisor_email='ANGEL.PEZO@APCASESORIAS.CL', + receptor_email=None, + )) + + def test_parse_dte_xml_fail_x(self) -> None: + # TODO: implement more cases + pass - # TODO: implement - pass + def test_parse_dte_xml_fail_1(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_bad_xml_1_xml_bytes) + + with self.assertRaises(ValueError) as cm: + parse_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Top level XML element 'Document' is required.", ) + ) + + def test_parse_dte_xml_fail_2(self) -> None: + xml_doc = xml_utils.parse_untrusted_xml(self.dte_bad_xml_2_xml_bytes) + + with self.assertRaises(ValueError) as cm: + parse_dte_xml(xml_doc) + self.assertSequenceEqual( + cm.exception.args, + ("Top level XML element 'Document' is required.", ) + )