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];
+ }
+}));