diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4155fdfe..93d45118 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,149 +9,193 @@ on: schedule: - cron: '0 16 * * 0' # sunday 16:00 +# Actions +# shivammathur/setup-php@v2 - https://github.com/marketplace/actions/setup-php-action +# nosborn/github-action-markdown-cli@v1.1.1 https://github.com/marketplace/actions/markdownlint-cli +# Tiryoh/actions-mkdocs@v0 https://github.com/marketplace/actions/mkdocs-action + jobs: - # this job performs phpunit tests on linux, windows and all php supported versions - tests: - name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-systems }} - runs-on: ${{ matrix.operating-systems }} + phpcs: + name: Code style (phpcs) + runs-on: "ubuntu-latest" + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 # see https://github.com/marketplace/actions/setup-php-action + with: + php-version: '8.1' + coverage: none + tools: cs2pr, phpcs + env: + fail-fast: true + - name: Code style (phpcs) + run: phpcs -q --report=checkstyle src/ tests/ | cs2pr + + php-cs-fixer: + name: Code style (php-cs-fixer) + runs-on: "ubuntu-latest" + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 # see https://github.com/marketplace/actions/setup-php-action + with: + php-version: '8.1' + coverage: none + tools: cs2pr, php-cs-fixer + env: + fail-fast: true + - name: Code style (php-cs-fixer) + run: php-cs-fixer fix --dry-run --format=checkstyle | cs2pr + + markdownlint: + name: Markdown style (markdownlint) + runs-on: "ubuntu-latest" + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Code style (markdownlint-cli) + uses: nosborn/github-action-markdown-cli@v3.2.0 + with: + files: '*.md docs/' + config_file: '.markdownlint.json' + + mkdocs: + name: Test docs building + runs-on: "ubuntu-latest" + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Run mkdocs + uses: Tiryoh/actions-mkdocs@v0 + with: + mkdocs_version: 'latest' + configfile: 'mkdocs.yml' + + phpstan: + name: Code analysis (phpstan) + runs-on: "ubuntu-latest" + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + coverage: none + tools: composer:v2, phpstan + env: + fail-fast: true + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install project dependencies + run: composer upgrade --no-interaction --no-progress --prefer-dist + - name: PHPStan + run: phpstan analyse --no-progress --verbose src/ tests/ + tests-linux: + name: Test PHP ${{ matrix.php-versions }} on Linux + runs-on: "ubuntu-latest" strategy: matrix: - operating-systems: [ "ubuntu-latest", "windows-latest" ] php-versions: [ '7.3', '7.4', '8.0', '8.1' ] - steps: - name: Checkout uses: actions/checkout@v3 - + with: + fetch-depth: 0 # required for scrutinizer - name: Install libsaxonb-java on linux - if: matrix.operating-systems == 'ubuntu-latest' run: | sudo apt-get update -y -qq sudo apt-get install -y -qq default-jre libsaxonb-java - - - name: Install saxonhe on windows - if: matrix.operating-systems == 'windows-latest' - run: choco install --ignore-checksums --no-progress --yes saxonhe - - # see https://github.com/marketplace/actions/setup-php-action + - name: Install dependencies running on nektos/act + if: github.actor == 'nektos/act' + run: sudo apt-get install -y -qq zstd - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php-versions }} - extensions: libxml, dom, xsl, simplexml, mbstring, openssl, soap, iconv, json, intl, fileinfo - coverage: none + coverage: xdebug tools: composer:v2 env: fail-fast: true - - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - - name: Install SAT XML resources shell: bash run: | git clone --depth 1 https://github.com/phpcfdi/resources-sat-xml resources-sat-xml-cloned mv resources-sat-xml-cloned/resources build/resources rm -r -f resources-sat-xml-cloned - - name: Install project dependencies run: | composer remove squizlabs/php_codesniffer friendsofphp/php-cs-fixer phpstan/phpstan --dev --no-interaction --no-progress --no-update composer upgrade --no-interaction --no-progress --prefer-dist - - name: Tests (phpunit) on linux - if: matrix.operating-systems == 'ubuntu-latest' - run: vendor/bin/phpunit --testdox --verbose - - - name: Tests (phpunit) on windows - if: matrix.operating-systems == 'windows-latest' - run: vendor/bin/phpunit --testdox --verbose - env: - saxonb-path: 'C:\ProgramData\chocolatey\bin\SaxonHE\bin\Transform.exe' - - # this job performs a full build (check style, testing with coverage, code analysis and build docs) - full-build: - name: Full build - runs-on: "ubuntu-latest" + run: vendor/bin/phpunit --testdox --verbose --coverage-clover=build/coverage-clover.xml + - name: Upload code coverage to scrutinizer + run: | + mkdir -p build/scrutinizer + composer require scrutinizer/ocular:dev-master --working-dir=build/scrutinizer --no-progress + php build/scrutinizer/vendor/bin/ocular code-coverage:upload -vvv --no-interaction --format=php-clover build/coverage-clover.xml + tests-windows: + name: Tests PHP ${{ matrix.php-versions }} on Windows + runs-on: "windows-latest" + strategy: + matrix: + php-versions: [ '7.3', '7.4', '8.0', '8.1' ] steps: - name: Checkout uses: actions/checkout@v3 - with: - fetch-depth: 0 - - # see https://github.com/marketplace/actions/setup-php-action + - name: Install saxonhe + run: choco install --ignore-checksums --no-progress --yes saxonhe - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: "8.0" - extensions: libxml, dom, xsl, simplexml, mbstring, openssl, soap, iconv, json, intl, fileinfo - coverage: xdebug - tools: composer:v2, cs2pr + php-version: ${{ matrix.php-versions }} + extensions: soap, intl, xsl, fileinfo + coverage: none + tools: composer:v2 env: fail-fast: true - - - name: Install libsaxonb-java on linux - if: matrix.operating-systems == 'ubuntu-latest' - run: | - sudo apt-get update -y -qq - sudo apt-get install -y -qq default-jre libsaxonb-java - - name: Get composer cache directory id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - + shell: bash + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - - name: Install SAT XML resources - run: bash tests/resource-sat-xml-download build/ - + shell: bash + run: | + git clone --depth 1 https://github.com/phpcfdi/resources-sat-xml resources-sat-xml-cloned + mv resources-sat-xml-cloned/resources build/resources + rm -r -f resources-sat-xml-cloned - name: Install project dependencies - run: composer upgrade --no-interaction --no-progress --prefer-dist - - # https://github.com/marketplace/actions/markdown-cli - - name: Code style (markdownlint-cli) - uses: nosborn/github-action-markdown-cli@v1.1.1 - with: - files: '*.md docs/' - config_file: '.markdownlint.json' - - - name: Code style (phpcs) - run: vendor/bin/phpcs -q --report=checkstyle src/ tests/ | cs2pr - - - name: Code style (php-cs-fixer) - run: vendor/bin/php-cs-fixer fix --dry-run --format=checkstyle | cs2pr - - - name: Tests (phpunit) - run: vendor/bin/phpunit --testdox --verbose --coverage-clover=build/coverage-clover.xml - - - name: Code analysis (phpstan) - run: vendor/bin/phpstan analyse --no-progress --verbose src/ tests/ - - - name: Upload code coverage to scrutinizer run: | - mkdir -p build/scrutinizer - composer require scrutinizer/ocular:dev-master --working-dir=build/scrutinizer --no-progress - php build/scrutinizer/vendor/bin/ocular code-coverage:upload -vvv --no-interaction --format=php-clover build/coverage-clover.xml - - # see https://github.com/marketplace/actions/mkdocs-action - - name: Run mkdocs - uses: Tiryoh/actions-mkdocs@v0 - with: - mkdocs_version: 'latest' - configfile: 'mkdocs.yml' + composer remove squizlabs/php_codesniffer friendsofphp/php-cs-fixer phpstan/phpstan --dev --no-interaction --no-progress --no-update + composer upgrade --no-interaction --no-progress --prefer-dist + - name: Tests (phpunit) + run: vendor/bin/phpunit --testdox --verbose + env: + saxonb-path: 'C:\ProgramData\chocolatey\bin\SaxonHE\bin\Transform.exe' diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index c33d0bf5..47a6c002 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -32,6 +32,23 @@ - Merge methods from `\CfdiUtils\Nodes\NodeHasValueInterface` into `\CfdiUtils\Nodes\NodeInterface`. - Remove deprecated constant `CfdiUtils\Retenciones\Retenciones::RET_NAMESPACE`. +## Version 2.23.4 2022-12-07 + +This is a maintenance release fo fix the continuous integration workflow and append pending development changes. + +- Fix test `CertificadoTest::testConstructWithValidExample()` to allow quoted slashes on name. +- Add *phpdoc* to the method `Certificate::getCertificateName()`. + The value can contain quoted slashes `\/` depending on the OpenSSL version. +- Update script `tests/validate.php` to validate CFDI 3.3 or CFDI 4.0. +- Add return types to some methods: + - `Status::comparableValue` and `Status::__toString`. + - `Discoverer::discoverInFile`. +- Improve `TestCase::installCertificate()`: It doesn't depend on the certificate's file name to install correctly. +- Update GitHub build workflow: + - Update GH Workflows: Remove deprecated `::set-output` & `::save-state`. + - Split **full build** actions to individual jobs. + - Split Windows and Linux testing. + ## Version 2.23.3 2022-08-11 Fix CFDI 4.0, must include `Comprobante/Impuestos/Traslados/Traslado@TipoFactor=Exento` when exists at least one diff --git a/src/CfdiUtils/Certificado/Certificado.php b/src/CfdiUtils/Certificado/Certificado.php index b0d10cb0..0d007af9 100644 --- a/src/CfdiUtils/Certificado/Certificado.php +++ b/src/CfdiUtils/Certificado/Certificado.php @@ -41,7 +41,7 @@ class Certificado * * @param string $filename Allows filename or certificate contents (PEM or DER) * @param OpenSSL|null $openSSL - * @throws \UnexpectedValueException when the certificate does not exists or is not readable + * @throws \UnexpectedValueException when the certificate does not exist or is not readable * @throws \UnexpectedValueException when cannot read the certificate or is empty * @throws \RuntimeException when cannot parse the certificate or is empty * @throws \RuntimeException when cannot get serialNumberHex or serialNumber from certificate @@ -100,7 +100,7 @@ private function extractPemCertificate(string $contents): string { $openssl = $this->getOpenSSL(); $decoded = @base64_decode($contents, true) ?: ''; - if ('' !== $decoded && $contents === base64_encode($decoded)) { // is a one liner certificate + if ('' !== $decoded && $contents === base64_encode($decoded)) { // is a one-liner certificate $doubleEncoded = $openssl->readPemContents($decoded)->certificate(); if ('' !== $doubleEncoded) { return $doubleEncoded; @@ -129,9 +129,9 @@ private function obtainPemCertificate(string $contents): string * * @return bool * - * @throws \UnexpectedValueException if the file does not exists or is not readable + * @throws \UnexpectedValueException if the file does not exist or is not readable * @throws \UnexpectedValueException if the file is not a PEM private key - * @throws \RuntimeException if cannot open the private key file + * @throws \RuntimeException if the private key file cannot be opened */ public function belongsTo(string $pemKeyFile, string $passPhrase = ''): bool { @@ -165,6 +165,12 @@ public function getRfc(): string return $this->rfc; } + /** + * Certificate name value as returned by openssl. + * In come cases (openssl version 3) it contains quoted slashes (\/) + * + * @return string + */ public function getCertificateName(): string { return $this->certificateName; @@ -180,7 +186,7 @@ public function getName(): string } /** - * Certificate serial number as ASCII, this data is in the format required by CFDI + * Return the certificate serial number ASCII formatted, this data is in the format required by CFDI * @return string */ public function getSerial(): string @@ -221,7 +227,7 @@ public function getPubkey(): string } /** - * Place where the certificate was when loaded, it might not exists on the file system + * Place where the certificate was when loaded, it might not exist on the file system * @return string */ public function getFilename(): string @@ -256,7 +262,7 @@ public function getPemContentsOneLine(): string * * @return bool * - * @throws \RuntimeException if cannot open the public key from certificate + * @throws \RuntimeException if the public key on the certificate cannot be opened * @throws \RuntimeException if openssl report an error */ public function verify(string $data, string $signature, int $algorithm = OPENSSL_ALGO_SHA256): bool @@ -281,7 +287,7 @@ public function verify(string $data, string $signature, int $algorithm = OPENSSL /** * @param string $filename - * @throws \UnexpectedValueException when the file does not exists or is not readable + * @throws \UnexpectedValueException when the file does not exist or is not readable * @return void */ protected function assertFileExists(string $filename) @@ -289,7 +295,7 @@ protected function assertFileExists(string $filename) $exists = false; $previous = null; try { - if (boolval(preg_match('/[[:cntrl:]]/', $filename))) { + if (preg_match('/[[:cntrl:]]/', $filename)) { $filename = '(invalid file name)'; throw new \RuntimeException('The file name contains control characters, it might be a DER content'); } diff --git a/src/CfdiUtils/Validate/Asserts.php b/src/CfdiUtils/Validate/Asserts.php index 3aafa5c6..02007280 100644 --- a/src/CfdiUtils/Validate/Asserts.php +++ b/src/CfdiUtils/Validate/Asserts.php @@ -62,7 +62,7 @@ public function putStatus(string $code, Status $status = null, string $explanati /** * Get and or set the flag that alerts about stop flow - * Consider this flag as: "Something was found, you chould not continue" + * Consider this flag as: "Something was found, you must not continue" * * @param bool|null $newValue value of the flag, if null then will not change the flag * @return bool the previous value of the flag @@ -96,7 +96,7 @@ public function hasWarnings(): bool * @param Status $status * @return Assert|null */ - public function getFirstStatus(Status $status) + public function getFirstStatus(Status $status): ?Assert { foreach ($this->asserts as $assert) { if ($status->equalsTo($assert->getStatus())) { @@ -127,7 +127,7 @@ public function get(string $code): Assert throw new \RuntimeException("There is no assert with code $code"); } - public function exists(string $code) + public function exists(string $code): bool { return array_key_exists($code, $this->asserts); } diff --git a/src/CfdiUtils/Validate/CfdiValidatorTrait.php b/src/CfdiUtils/Validate/CfdiValidatorTrait.php index 448713a8..d29122b7 100644 --- a/src/CfdiUtils/Validate/CfdiValidatorTrait.php +++ b/src/CfdiUtils/Validate/CfdiValidatorTrait.php @@ -33,11 +33,11 @@ public function __construct(XmlResolver $xmlResolver = null, XsltBuilderInterfac /** * Validate and return the asserts from the validation process. * This method can use a xml string and a NodeInterface, - * is your responsability that the node is the representation of the content. + * is your responsibility that the node is the representation of the content. * * @param string $xmlString * @param NodeInterface $node - * @return Asserts|\CfdiUtils\Validate\Assert[] + * @return Asserts|Assert[] */ public function validate(string $xmlString, NodeInterface $node): Asserts { @@ -63,7 +63,7 @@ public function validate(string $xmlString, NodeInterface $node): Asserts * Validate and return the asserts from the validation process based on a xml string * * @param string $xmlString - * @return Asserts|\CfdiUtils\Validate\Assert[] + * @return Asserts|Assert[] */ public function validateXml(string $xmlString): Asserts { @@ -74,7 +74,7 @@ public function validateXml(string $xmlString): Asserts * Validate and return the asserts from the validation process based on a node interface object * * @param NodeInterface $node - * @return Asserts|\CfdiUtils\Validate\Assert[] + * @return Asserts|Assert[] */ public function validateNode(NodeInterface $node): Asserts { diff --git a/src/CfdiUtils/Validate/Discoverer.php b/src/CfdiUtils/Validate/Discoverer.php index 7367139d..bf15f2cb 100644 --- a/src/CfdiUtils/Validate/Discoverer.php +++ b/src/CfdiUtils/Validate/Discoverer.php @@ -35,7 +35,7 @@ public function discoverInFolder(string $namespacePrefix, string $directoryPath) * @param string $filename * @return ValidatorInterface|null */ - public function discoverInFile(string $namespacePrefix, string $filename) + public function discoverInFile(string $namespacePrefix, string $filename): ?ValidatorInterface { $basename = basename($filename); $classname = $this->castNamespacePrefix($namespacePrefix) . substr($basename, 0, strlen($basename) - 4); diff --git a/src/CfdiUtils/Validate/Status.php b/src/CfdiUtils/Validate/Status.php index fe3cc909..20b484b1 100644 --- a/src/CfdiUtils/Validate/Status.php +++ b/src/CfdiUtils/Validate/Status.php @@ -4,7 +4,7 @@ /** * Status (immutable value object) - * Define the status used in an assert + * Define the status used in an assertion */ class Status { @@ -90,12 +90,12 @@ public function compareTo(self $status): int return $this->comparableValue($this) <=> $this->comparableValue($status); } - public static function comparableValue(self $status) + public static function comparableValue(self $status): int { return self::ORDER_MAP[$status->status]; } - public function __toString() + public function __toString(): string { return $this->status; } diff --git a/tests/CfdiUtilsTests/Certificado/CertificadoTest.php b/tests/CfdiUtilsTests/Certificado/CertificadoTest.php index 08c83cab..0cf4062b 100644 --- a/tests/CfdiUtilsTests/Certificado/CertificadoTest.php +++ b/tests/CfdiUtilsTests/Certificado/CertificadoTest.php @@ -13,17 +13,17 @@ public function testConstructWithValidExample() // openssl x509 -nameopt utf8,sep_multiline,lname -inform DER -noout -dates -serial -subject \ // -fingerprint -pubkey -in tests/assets/certs/EKU9003173C9.cer $expectedPublicKey = <<< EOD ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjdKXiqYHzi++YmEb9X6q -vqFWLCz1VEfxom2JhinPSJxxcuZWBejk2I5yCL5pDnUaG2xpQlMTkV/7S7JfGGvY -JumKO4R5zg0QSA7qdxiEhcwf/ekfSvzM2EDnLHDCKAQwEWsnJy78uxZTLzu/65VZ -7EgEcWUTvCs/GZJLI9s6XmKY2SMmv9+vfqBqkJNXE0ZB6OfSbyeE325P94iMn+B/ -yJ4vZwXvXGFqNDJyqG+ww7f77HYubQPJjLQPedy2qTcgmSAwkUEJVBjYA6mPf/Be -ZlL1YJHHM7CIBnb3/bzED0n944woio+4+rnMZdfhcCVpm74DZomlEf9KuJtq5u/J -RQIDAQAB ------END PUBLIC KEY----- - -EOD; + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjdKXiqYHzi++YmEb9X6q + vqFWLCz1VEfxom2JhinPSJxxcuZWBejk2I5yCL5pDnUaG2xpQlMTkV/7S7JfGGvY + JumKO4R5zg0QSA7qdxiEhcwf/ekfSvzM2EDnLHDCKAQwEWsnJy78uxZTLzu/65VZ + 7EgEcWUTvCs/GZJLI9s6XmKY2SMmv9+vfqBqkJNXE0ZB6OfSbyeE325P94iMn+B/ + yJ4vZwXvXGFqNDJyqG+ww7f77HYubQPJjLQPedy2qTcgmSAwkUEJVBjYA6mPf/Be + ZlL1YJHHM7CIBnb3/bzED0n944woio+4+rnMZdfhcCVpm74DZomlEf9KuJtq5u/J + RQIDAQAB + -----END PUBLIC KEY----- + + EOD; $cerfile = $this->utilAsset('certs/EKU9003173C9.cer'); $certificado = new Certificado($cerfile); @@ -37,7 +37,7 @@ public function testConstructWithValidExample() '/serialNumber= / XIQB891116MGRMZR05', '/OU=Escuela Kemper Urgate', ]); - $this->assertSame($certificateName, $certificado->getCertificateName()); + $this->assertSame($certificateName, str_replace('\/', '/', $certificado->getCertificateName())); $this->assertSame('ESCUELA KEMPER URGATE SA DE CV', $certificado->getName()); $this->assertSame('EKU9003173C9', $certificado->getRfc()); $this->assertSame('30001000000400002434', $certificado->getSerial()); diff --git a/tests/CfdiUtilsTests/Cleaner/CleanerTest.php b/tests/CfdiUtilsTests/Cleaner/CleanerTest.php index 9b5ff405..bc708b2e 100644 --- a/tests/CfdiUtilsTests/Cleaner/CleanerTest.php +++ b/tests/CfdiUtilsTests/Cleaner/CleanerTest.php @@ -202,23 +202,23 @@ public function testRemoveNonSatNSschemaLocationsRemoveEmptySchemaLocation() public function testCollapseComprobanteComplemento() { $source = <<<'EOT' - - - - - - -EOT; + + + + + + + EOT; $expected = <<<'EOT' - - - - - - - - -EOT; + + + + + + + + + EOT; $cleaner = new Cleaner($source); $cleaner->collapseComprobanteComplemento(); $this->assertXmlStringEqualsXmlString($expected, $cleaner->retrieveXml()); diff --git a/tests/CfdiUtilsTests/PemPrivateKey/PemPrivateKeyTest.php b/tests/CfdiUtilsTests/PemPrivateKey/PemPrivateKeyTest.php index 4d3bc4f9..3b120a44 100644 --- a/tests/CfdiUtilsTests/PemPrivateKey/PemPrivateKeyTest.php +++ b/tests/CfdiUtilsTests/PemPrivateKey/PemPrivateKeyTest.php @@ -130,12 +130,12 @@ public function testSign() $content = 'lorem ipsum'; $expectedSign = <<< EOC -CzjYgB2dOp4P76kYBSGymRJdQo9hjErCF+5mvoiVWVnvcV/eg9IkW+1DnOem5slYzU9+lzOo+I79wcOe -0gRtsmybGnViXxAQ8rr7YciFCoyqtKhxGjdgBpvO2NMT84n6U8ChYb8v7O/s4Gi5yTPj9D113rNsQGb8 -5nXerA+N6G6axy0F/IcUMZ6VPkDDjATcwjEj5A3q7qORG/l2cAKaV4nGKjn8V82bZ40ys7PGvFfZfirZ -BeKg1QPUqf2fpgVI6wf/IM4YRD6ZbTgtFiYH30/dlzowZTAR1NMHJXa4uxCdTY7mQVekTw0FNDxrAZr/ -5lLezLMMyEezIz+EQKgAvg== -EOC; + CzjYgB2dOp4P76kYBSGymRJdQo9hjErCF+5mvoiVWVnvcV/eg9IkW+1DnOem5slYzU9+lzOo+I79wcOe + 0gRtsmybGnViXxAQ8rr7YciFCoyqtKhxGjdgBpvO2NMT84n6U8ChYb8v7O/s4Gi5yTPj9D113rNsQGb8 + 5nXerA+N6G6axy0F/IcUMZ6VPkDDjATcwjEj5A3q7qORG/l2cAKaV4nGKjn8V82bZ40ys7PGvFfZfirZ + BeKg1QPUqf2fpgVI6wf/IM4YRD6ZbTgtFiYH30/dlzowZTAR1NMHJXa4uxCdTY7mQVekTw0FNDxrAZr/ + 5lLezLMMyEezIz+EQKgAvg== + EOC; $sign = chunk_split(base64_encode($privateKey->sign($content, OPENSSL_ALGO_SHA256)), 80, "\n"); $this->assertEquals($expectedSign, rtrim($sign)); } diff --git a/tests/CfdiUtilsTests/TestCase.php b/tests/CfdiUtilsTests/TestCase.php index 3600c54c..0b2d8ab9 100644 --- a/tests/CfdiUtilsTests/TestCase.php +++ b/tests/CfdiUtilsTests/TestCase.php @@ -2,6 +2,7 @@ namespace CfdiUtilsTests; +use CfdiUtils\Certificado\Certificado; use CfdiUtils\Certificado\SatCertificateNumber; use CfdiUtils\XmlResolver\XmlResolver; @@ -57,11 +58,13 @@ public function providerFullJoin(array $first, array ...$next): array protected function installCertificate(string $cerfile): string { - $certificateNumber = substr(basename($cerfile), 0, 20); - $satCertificateNumber = new SatCertificateNumber($certificateNumber); + $resolver = $this->newResolver(); - $cerRetriever = $this->newResolver()->newCerRetriever(); + $certificate = new Certificado('file://' . $cerfile); + $certificateNumber = $certificate->getSerial(); + $satCertificateNumber = new SatCertificateNumber($certificateNumber); + $cerRetriever = $resolver->newCerRetriever(); $installationPath = $cerRetriever->buildPath($satCertificateNumber->remoteUrl()); if (file_exists($installationPath)) { return $installationPath; diff --git a/tests/resource-sat-xml-download b/tests/resource-sat-xml-download index 4f8abb05..025eecee 100755 --- a/tests/resource-sat-xml-download +++ b/tests/resource-sat-xml-download @@ -7,15 +7,25 @@ else fi WORKDIR="$(mktemp --directory)" +SOURCEFILE=https://github.com/phpcfdi/resources-sat-xml/archive/master.zip ZIPFILE="$WORKDIR/resources-sat-xml.zip" # download latest archive from github as resources-sat-xml.zip -echo "Downloading https://github.com/phpcfdi/resources-sat-xml/archive/master.zip to $ZIPFILE" -wget -O "$ZIPFILE" https://github.com/phpcfdi/resources-sat-xml/archive/master.zip +echo "Downloading $SOURCEFILE to $ZIPFILE" +wget -O "$ZIPFILE" "$SOURCEFILE" +if [ $? -ne 0 ]; then + echo "Error while downloading $SOURCEFILE" >&2 + exit 1 +fi # unzip the "resources" folder contents and place then into my-resources echo "Extract resources from $ZIPFILE" unzip "$ZIPFILE" 'resources-sat-xml-master/resources/*' -d "$WORKDIR" +wget -O "$ZIPFILE" "$SOURCEFILE" +if [ $? -ne 0 ]; then + echo "Error while extract resources from $ZIPFILE" >&2 + exit 1 +fi echo "Copy $WORKDIR/resources-sat-xml-master/resources/ to $DESTINATION" rm -rf "$DESTINATION/resources/www.sat.gob.mx" diff --git a/tests/validate.php b/tests/validate.php index 0a13e821..0ddcbfff 100644 --- a/tests/validate.php +++ b/tests/validate.php @@ -1,60 +1,177 @@ getXmlResolver()->setLocalPath(''); - } - foreach ($files as $file) { - $xmlContent = strval(file_get_contents($file)); +exit(call_user_func(new class(...$argv) { + /** @var string */ + private $command; + + /** @var string[] */ + private $arguments; + + /** @var array */ + private $validators; + + private const SUCCESS = 0; + + private const ERROR = 1; + + private const FAILURE = 2; + + public function __construct(string $command, string ...$arguments) + { + $this->command = $command; + $this->arguments = $arguments; + $this->validators = [ + '3.3' => new CfdiValidator33(), + '4.0' => new CfdiValidator40(), + ]; + } + + public function __invoke(): int + { + if ([] !== array_intersect(['-h', '--help'], $this->arguments)) { + $this->printHelp(); + return self::SUCCESS; + } + + $files = []; + $noCache = false; + $clean = false; + foreach ($this->arguments as $argument) { + if (in_array($argument, ['-c', '--clean'], true)) { + $clean = true; + continue; + } + if ('--no-cache' === $argument) { + $noCache = true; + continue; + } + $files[] = $argument; + } + $files = array_unique(array_filter($files)); + if ([] === $files) { + printf("FAIL: No files were specified\n"); + return 2; + } + + if ($noCache) { + foreach ($this->validators as $validator) { + $validator->getXmlResolver()->setLocalPath(''); + } + } + + set_error_handler(function (int $number, string $message) { + throw new Error($message, $number); + }); + + $exitCode = self::SUCCESS; + foreach ($files as $file) { + printf("File: %s\n", $file); + try { + $asserts = $this->validateFile($file, $clean); + } catch (Throwable $exception) { + printf("FAIL: (%s) %s\n\n", get_class($exception), $exception->getMessage()); + $exitCode = self::FAILURE; + continue; + } + if (! $this->printAsserts($asserts)) { + $exitCode = self::ERROR; + } + printf("\n"); + } + + return $exitCode; + } + + private function printHelp(): void + { + $command = basename($this->command); + echo <<< EOH + $command Validates CFDI files + Syntax: + $command [-h|--help] [-c|--clean] [--no-cache] cfdi.xml ... + Arguments: + -h, --help Show this help + -c, --clean Clean CFDI before validation + --no-cache Tell resolver to not use local cache + cfdi.xml Files to check, as many as needed + Exit codes: + 0 - All files were validated with success + 1 - At least one file contains errors or warnings + 2 - At least one file produce an exception + + WARNING: This program can change at any time! Do not depend on this file or its results! + + + EOH; + } + + private function printAsserts(Asserts $asserts): bool + { + $warnings = $asserts->warnings(); + $errors = $asserts->errors(); + printf( + "Asserts: %s total, %s executed, %s ignored, %s success, %s warnings, %s errors.\n", + $asserts->count(), + $asserts->count() - count($asserts->nones()), + count($asserts->nones()), + count($asserts->oks()), + count($warnings), + count($errors) + ); + foreach ($warnings as $warning) { + $this->printAssert('WARNING', $warning); + } + foreach ($errors as $error) { + $this->printAssert('ERROR', $error); + } + return [] === $errors && [] === $warnings; + } + + private function printAssert(string $type, Assert $assert): void + { + $explanation = ''; + if ($assert->getExplanation()) { + $explanation = sprintf("\n%s%s", str_repeat(' ', mb_strlen($type) + 2), $assert->getExplanation()); + } + printf("%s: %s - %s%s\n", $type, $assert->getCode(), $assert->getTitle(), $explanation); + } + + private function validateFile(string $file, bool $clean): Asserts + { + $xmlContent = (string) file_get_contents($file); if ($clean) { - $xmlContent = \CfdiUtils\Cleaner\Cleaner::staticClean($xmlContent); + $xmlContent = Cleaner::staticClean($xmlContent); } - $asserts = $validator->validateXml($xmlContent); - print_r(array_filter([ - 'file' => $file, - 'asserts' => $asserts->count(), - 'hasErrors' => $asserts->hasErrors() ? 'yes' : 'no', - 'errors' => ($asserts->hasErrors()) ? $asserts->errors() : null, - 'hasWarnings' => $asserts->hasWarnings() ? 'yes' : 'no', - 'warnings' => ($asserts->hasWarnings()) ? $asserts->warnings() : null, - ])); + return $this->validateXmlContent($xmlContent); + } + + private function validateXmlContent(string $xmlContent): Asserts + { + $cfdi = Cfdi::newFromString($xmlContent); + return $this->validateCfdi($cfdi); } - return 0; -}, ...$argv)); + private function validateCfdi(Cfdi $cfdi): Asserts + { + $validator = $this->getValidatorForVersion($cfdi->getVersion()); + return $validator->validate($cfdi->getSource(), $cfdi->getNode()); + } + + /** @return CfdiValidator33|CfdiValidator40 */ + private function getValidatorForVersion(string $version) + { + if (! isset($this->validators[$version])) { + throw new Exception(sprintf('There is no validator for "%s"', $version)); + } + return $this->validators[$version]; + } +}));