diff --git a/.travis.yml b/.travis.yml index 1440c7e..6d82e29 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: php # php compatibility php: - - 5.6 - 7.0 - 7.1 @@ -13,7 +12,6 @@ sudo: false before_script: - travis_retry composer self-update - travis_retry composer install --no-interaction --prefer-dist - - test $TRAVIS_PHP_VERSION == '7.1' && travis_retry composer require --dev --no-interaction --prefer-dist scrutinizer/ocular || true script: - mkdir -p build/tests/ @@ -23,7 +21,11 @@ script: - vendor/bin/phpunit --coverage-text --coverage-clover=build/tests/coverage.xml after_script: - - if [[ $TRAVIS_PHP_VERSION == '7.1' ]]; then php vendor/bin/ocular code-coverage:upload --format=php-clover build/tests/coverage.xml; fi + - | + if [[ $TRAVIS_PHP_VERSION == '7.0' ]]; then + wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover build/tests/coverage.xml + fi notifications: email: false diff --git a/CHANGELOG.md b/CHANGELOG.md index fd5fe29..05197f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# Version 2.0.0 +- This version does not include `Locator` nor `DownloaderInterface` implementations. + That functionality is actually outside the scope of this library and that is the reason + why it was removed. A new library was created to implement this, take a look in + `eclipxe/xmlresourceretriever` https://github.com/eclipxe13/XmlResourceRetriever/ +- Constructor of `SchemaValidator` and `Schemas` changed. +- Add new method `SchemaValidator::validateWithSchemas` that do the same + thing than `SchemaValidator::validate` but you must provide the `Schemas` collection +- Change from `protected` to `public` the method `SchemaValidator::buildSchemas`, + it's usefull when used with `SchemaValidator::validateWithSchemas` to change + XSD remote locations to local or other places. +- Add `XmlSchemaValidator::LibXmlException`. It contains a method to exec a callable + isolating the use internal errors setting and other to collect libxml errors + and throw it like an exception. +- Rename `Schemas::getXsd` to `Schemas::getImporterXsd` +- Remove compatibility with PHP 5.6, minimum version is now PHP 7.0 +- Add scalar type declarations +- Remove test assets from Mexican SAT +- Tests: Move files served by php built-in web server to from assets to public + # Version 1.1.4 - Fix implementation of libxml use internal errors on `SchemaValidator::validate` - When creating the dom document avoid warnings (fix using the correct constant) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 769e2ae..6e9199f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,27 +50,41 @@ We don't enjoy rejecting your hard work, but some features just don't fit with t When you do begin working on your feature, here are some guidelines to consider: * Your pull request description should clearly detail the changes you have made. -* We following by minimum the **[PSR-2 coding standard](http://www.php-fig.org/psr/psr-2/)**. Please ensure your code does, too. +* Follow our code style using `squizlabs/php_codesniffer` and `friendsofphp/php-cs-fixer`. * Please **write tests** for any new features you add. * Please **ensure that tests pass** before submitting your pull request. We have Travis CI automatically running tests for pull requests. However, running the tests locally will help save time. * **Use topic/feature branches.** Please do not ask us to pull from your master branch. * **Submit one feature per pull request.** If you have multiple features you wish to submit, please break them up into separate pull requests. * **Send coherent history**. Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. +## Check the code style + If you are having issues with coding standars use `php-cs-fixer` and `phpcbf` -```bash +```shell vendor/bin/php-cs-fixer fix -v vendor/bin/phpcbf src/ tests/ ``` ## Running Tests -The following tests must pass before we will accept a pull request. If any of these do not pass, -it will result in a complete build failure. Before you can run these, be sure to `composer install`. +The following tests must pass before we will accept a pull request. +If any of these do not pass, it will result in a complete build failure. +Before you can run these, be sure to `composer install` or `composer update`. -``` +```shell vendor/bin/parallel-lint src/ tests/ vendor/bin/phpcs -sp src/ tests/ +vendor/bin/php-cs-fixer fix -v --dry-run vendor/bin/phpunit --coverage-text ``` + +## web server instance while running tests + +The phpunit process in the bootstrap step uses the PHP built-in web server in port 8999 to serve the contents +of `tests/public` in order to test the validation process on remote resources. + +When the phpunit process ends, the web server instance is killed. + +Take a look in `tests/boostrap.php` to see how this is working. + diff --git a/README.md b/README.md index d122d4f..2b6d952 100644 --- a/README.md +++ b/README.md @@ -11,47 +11,65 @@ This is a library to validate XML files against multiple XSD Schemas according to its own definitions. -I create this project because I need to validate XML documents against not-always-known schemas -that have to be downloaded from Internet. - The way this works is: -1. Receive a valid xml file +1. Receive a valid xml string as the content to be evaluated 2. Scan the file for every schemaLocation -3. Compose a file that include all the schemas +3. Compose a schema that include all the schemas 4. Validate the XML against the composed file -Features: -- It can have a repository for external schemas -- Retrieve schemas from Internet -- Storage the schemas until they expire - ## Installation -Use composer, so please run `composer require eclipxe/xmlschemavalidator` or include this on your `composer.json` file: - -```json -{ - "require": { - "eclipxe/xmlschemavalidator": "@stable" - } -} +Use [composer](https://getcomposer.org/), so please run +```shell +composer require eclipxe/xmlschemavalidator ``` ## Basic usage + ```php validate(file_get_contents('example.xml')); -if (! $valid) { - echo $validator->getError(), "\n"; -} else { - echo "OK\n"; +$contents = file_get_contents('example.xml'); +$validator = new \XmlSchemaValidator\SchemaValidator($contents); +if (! $validator->validate()) { + echo 'Found error: ' . $validator->getLastError(); } +``` +## Advanced usage + +```php +buildSchemas(); +$schemas->create('http://example.org/schemas/x1', './local-schemas/x1.xsd'); + +// validateWithSchemas does not return boolean, it throws an exception +try { + $validator->validateWithSchemas($schemas)); +} catch (\XmlSchemaValidator\SchemaValidatorException $ex) { + echo 'Found error: ' . $ex->getMessage(); +} ``` +## About libxml errors + +This library depends on PHP libxml and uses internal errors `libxml_use_internal_errors` to retrieve +the errors when creates the `DOMDocument` or validate against the schema files. +Instead of raise an error it creates a `LibXmlException` with the errors chained. +It also restore the value of `libxml_use_internal_errors` after execution. + +## Version 1.x is deprecated + +Version 1.x is no longer on development. It has a problem of concerns, the same library try to solve two different +issues: Validate an XML file and store locally a copy of the XSD files. +Version 2.x breaks this problem and give this library only one propose: +Validate an XML file against its multiple XSD files, it does not matter where are located. + +Also, version 2.x uses PHP 7 with scalar type declarations, so it no longer compatible with PHP 5.6. + ## Contributing Contributions are welcome! Please read [CONTRIBUTING][] for details diff --git a/TODO.md b/TODO.md index 9005607..2be1a6f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,12 +1,12 @@ # eclipxe13/xmlschemavalidator To Do -- [ ] Deprecate PHP 5.6 to PHP 7.0 and phpunit from ^5.7 to ^6.3 - [ ] Document usage examples -- [ ] Move from standard exceptions to library exceptions -- [ ] Use better XSD samples, currently is heavely related to Mexico SAT CFDI v 3.2 ## Done +- [X] Deprecate PHP 5.6 to PHP 7.0 and phpunit from ^5.7 to ^6.3 +- [X] Move from standard exceptions to library exceptions +- [X] Use better XSD samples, currently is heavely related to Mexico SAT CFDI v 3.2 - [X] Create a downloader object to separate responsabilities of Locator object - [X] Include contribute and CoC - [X] ~~Full coverage~~ Coverage over 90% diff --git a/composer.json b/composer.json index 8a8aeec..e0a6a4c 100644 --- a/composer.json +++ b/composer.json @@ -11,10 +11,11 @@ } ], "require": { - "php": ">=5.6" + "php": ">=7.0", + "ext-xml": "*" }, "require-dev": { - "phpunit/phpunit": "^5.7", + "phpunit/phpunit": "^6.2", "jakub-onderka/php-parallel-lint": "^0.9", "squizlabs/php_codesniffer": "^3.0", "friendsofphp/php-cs-fixer": "^2.4" diff --git a/src/XmlSchemaValidator/Downloader/CurlDownloader.php b/src/XmlSchemaValidator/Downloader/CurlDownloader.php deleted file mode 100644 index 0588a2d..0000000 --- a/src/XmlSchemaValidator/Downloader/CurlDownloader.php +++ /dev/null @@ -1,38 +0,0 @@ - [ - 'timeout' => $timeout, - 'ignore_errors' => false, - ], - ]); - // if Error Control '@' is omitted then when the download fail an error occurs and cannot return the exception - if (! @copy($url, $tempname, $ctx)) { - unlink($tempname); - throw new \RuntimeException("Download fail for url $url"); - } - return $tempname; - } -} diff --git a/src/XmlSchemaValidator/FileMimeChecker.php b/src/XmlSchemaValidator/FileMimeChecker.php deleted file mode 100644 index 4a5684b..0000000 --- a/src/XmlSchemaValidator/FileMimeChecker.php +++ /dev/null @@ -1,52 +0,0 @@ -getMimeFileInfo) { - $this->getMimeFileInfo = new \finfo(FILEINFO_SYMLINK); - } - return $this->getMimeFileInfo; - } - - public function getMimeType($filename) - { - if (! (is_file($filename) || is_link($filename)) || ! is_readable($filename)) { - return ''; - } - return (string) $this->getMimeFileInfo()->file($filename, FILEINFO_MIME_TYPE); - } - - public function check($filename) - { - return $this->checkMime($this->getMimeType($filename)); - } - - public function checkMime($mimetype) - { - $mimetype = $this->cast($mimetype); - if (0 === $this->count()) { - return true; - } - if ('' === $mimetype) { - return false; - } - return array_key_exists($mimetype, $this->members); - } - - public function cast($member) - { - return strtolower(trim(parent::cast($member))); - } -} diff --git a/src/XmlSchemaValidator/LibXmlException.php b/src/XmlSchemaValidator/LibXmlException.php new file mode 100644 index 0000000..cc72f83 --- /dev/null +++ b/src/XmlSchemaValidator/LibXmlException.php @@ -0,0 +1,54 @@ +message, 0, $lastException); + $lastException = $current; + } + if (null !== $lastException) { + throw $lastException; + } + } + + /** + * Execute a callable ensuring that the execution will occur inside an environment + * where libxml use internal errors is true. + * + * After executing the callable the value of libxml use internal errors is set to + * previous value. + * + * @param callable $callable + * @return mixed + * + * @throws LibXmlException if some error inside libxml was found + */ + public static function useInternalErrors(callable $callable) + { + $previousLibXmlUseInternalErrors = libxml_use_internal_errors(true); + if ($previousLibXmlUseInternalErrors) { + libxml_clear_errors(); + } + $return = $callable(); + try { + static::throwFromLibXml(); + } finally { + libxml_use_internal_errors($previousLibXmlUseInternalErrors); + } + return $return; + } +} diff --git a/src/XmlSchemaValidator/Locator.php b/src/XmlSchemaValidator/Locator.php deleted file mode 100644 index 7dc361f..0000000 --- a/src/XmlSchemaValidator/Locator.php +++ /dev/null @@ -1,287 +0,0 @@ -repository = (string) $repository; - $this->timeout = max(1, (integer) $timeout); - $this->expire = max(0, (integer) $expire); - $this->mimeChecker = new FileMimeChecker(); - $this->downloader = $downloader ? : new PhpDownloader(); - } - - /** - * Location of the local repository of cached files - * @return string - */ - public function getRepository() - { - return $this->repository; - } - - /** - * Seconds to timeout when a file is required - * @return int - */ - public function getTimeout() - { - return $this->timeout; - } - - /** - * Seconds to know if a cached file is expired - * @return int - */ - public function getExpire() - { - return $this->expire; - } - - /** - * @return DownloaderInterface - */ - public function getDownloader() - { - return $this->downloader; - } - - /** - * Return a filename for a given URL based on the registry. - * If the file is not registered then it is downloaded and stored in the repository location - * @param string $url - * @return string - */ - public function get($url) - { - $this->assertUrlIsValid($url); - // get the file or refresh the cache - $filename = $this->cacheFileName($url); - // register if not previously registered - if (! $this->registered($url) || $this->register[$url] == $filename) { - $filename = $this->cache($url); - $this->register($url, $filename); - } - return $this->register[$url]; - } - - /** - * Build a unique name for a url including the repository - * @param string $url - * @return string - */ - public function cacheFileName($url) - { - $this->assertUrlIsValid($url); - return $this->repository . DIRECTORY_SEPARATOR . 'cache-' . md5(strtolower($url)); - } - - /** - * Register a url with a file without download it. However the file must exists and be readable. - * @param string $url - * @param string $filename - */ - public function register($url, $filename) - { - $this->assertUrlIsValid($url); - $mimetype = $this->mimeChecker->getMimeType($filename); - if ('' === $mimetype) { - throw new \RuntimeException("File $filename does not exists or is not readable"); - } - if (! $this->mimeChecker->checkMime($mimetype)) { - throw new \RuntimeException("File $filename is not a valid mime type"); - } - $this->register[$url] = $filename; - } - - /** - * Unregister a url from the cache - * @param string $url - */ - public function unregister($url) - { - unset($this->register[(string) $url]); - } - - /** - * Return a copy of the registry - * @return array - */ - public function registry() - { - return $this->register; - } - - /** - * Return if a given url exists in the registry - * @param string $url - * @return bool - */ - public function registered($url) - { - return array_key_exists($url, $this->register); - } - - /** - * Return the filename of an url, of needs to download a new copy then - * it try to download, validate the mime of the downloaded file and place the file on the repository - * @param string $url - * @return string - */ - protected function cache($url) - { - // get the filename - $filename = $this->cacheFileName($url); - // if no need to download then return - if (! $this->needToDownload($filename)) { - return $filename; - } - // download the file and set into a temporary file - $temporal = $this->downloader->download($url, $this->getTimeout()); - if (! $this->mimeIsAllowed($temporal)) { - unlink($temporal); - throw new \RuntimeException("Downloaded file from $url is not a valid mime"); - } - // move temporal to final destination - // if $filename exists, it will be overwritten. - if (! @rename($temporal, $filename)) { - unlink($temporal); - throw new \RuntimeException("Cannot move the temporary file to $filename"); - } - // return the filename - return $filename; - } - - /** - * append a mime to the list of mimes allowed - * @param string $mime - */ - public function mimeAllow($mime) - { - $this->mimeChecker->add($mime); - } - - /** - * Remove a mime to the list of mimes allowed - * NOTE: This method does not affect previously registered urls - * @param string $mime - */ - public function mimeDisallow($mime) - { - $this->mimeChecker->remove($mime); - } - - /** - * return the list of allowed mimes - * @return bool - */ - public function mimeList() - { - return $this->mimeChecker->all(); - } - - /** - * check if a the mime of a file is allowed - * - * @param string $filename path to the file - * @return bool - */ - public function mimeIsAllowed($filename) - { - return $this->mimeChecker->check($filename); - } - - /** - * Internal function to assert if URL is valid, if not throw an exception - * @param string $url - * @throws \RuntimeException - */ - private function assertUrlIsValid($url) - { - if (empty($url)) { - throw new \RuntimeException('Url (empty) is not valid'); - } - if (! filter_var($url, FILTER_VALIDATE_URL)) { - throw new \RuntimeException("Url $url is not valid"); - } - } - - /** - * Rules to determine if the file needs to be downloaded: - * 1. file does not exists - * 2. files do not expire - * 3. file is expired - * @param string $filename - * @return bool - */ - protected function needToDownload($filename) - { - // the file does not exists -> yes - if (! file_exists($filename)) { - return true; - } - // the files stored never expire -> no need - if ($this->expire <= 0) { - return false; - } - // if aging of the file is more than the expiration then need to refresh - clearstatcache(false, $filename); - if (time() - filemtime($filename) > $this->expire) { - return true; - } - // no need to expire - return false; - } -} diff --git a/src/XmlSchemaValidator/Schema.php b/src/XmlSchemaValidator/Schema.php index 711acd8..0d7ef2e 100644 --- a/src/XmlSchemaValidator/Schema.php +++ b/src/XmlSchemaValidator/Schema.php @@ -2,10 +2,7 @@ namespace XmlSchemaValidator; /** - * Schema item, used by SchemaValidator and Schemas - * - * @access private - * @package XmlSchemaValidator + * Schema immutable object, used by SchemaValidator and Schemas */ class Schema { @@ -19,16 +16,16 @@ class Schema * @param string $namespace * @param string $location */ - public function __construct($namespace, $location) + public function __construct(string $namespace, string $location) { - $this->namespace = (string) $namespace; - $this->location = (string) $location; + $this->namespace = $namespace; + $this->location = $location; } /** * @return string */ - public function getNamespace() + public function getNamespace(): string { return $this->namespace; } @@ -36,7 +33,7 @@ public function getNamespace() /** * @return string */ - public function getLocation() + public function getLocation(): string { return $this->location; } diff --git a/src/XmlSchemaValidator/SchemaValidator.php b/src/XmlSchemaValidator/SchemaValidator.php index e6cfd89..84cccd9 100644 --- a/src/XmlSchemaValidator/SchemaValidator.php +++ b/src/XmlSchemaValidator/SchemaValidator.php @@ -5,141 +5,136 @@ use DOMXPath; /** - * This class is a SchemaValidator + * This class is an XML schema validator * It is needed because some XML can contain more than one external schema * and DOM library fails to load it. - * - * It uses Locator class to store locally the xsd files and build a generic - * import schema that uses that files and DOM library can perform the validations */ class SchemaValidator { - /** @var Locator */ - private $locator; + /** @var DOMDocument */ + private $document; + + /** @var string */ private $error = ''; /** - * Create the SchemaValidator - * @param Locator $locator if null a locator with default parameters is created + * SchemaValidator constructor. + * + * @param string $content + * @throws \InvalidArgumentException if content is empty + * @throws SchemaValidatorException if malformed xml content */ - public function __construct(Locator $locator = null) - { - $this->locator = ($locator) ?: new Locator(); - } - - public function getError() - { - return $this->error; - } - - public function getLocator() + public function __construct(string $content) { - return $this->locator; + if ('' === $content) { + throw new \InvalidArgumentException('The content to validate must be a non-empty string'); + } + $document = new DOMDocument(); + try { + LibXmlException::useInternalErrors(function () use ($content, $document) { + $document->loadXML($content, LIBXML_NOWARNING); + }); + } catch (LibXmlException $ex) { + throw new SchemaValidatorException('Malformed XML Document: ' . $ex->getMessage()); + } + $this->document = $document; } /** - * validate the content using the current locator - * @param string $content The XML content on UTF-8 + * Validate the content by: + * - Create the Schemas collection from the document + * - Validate using validateWithSchemas + * + * @see validateWithSchemas * @return bool */ - public function validate($content) + public function validate(): bool { - // encapsulate the function inside libxml_use_internal_errors(true) - if (true !== libxml_use_internal_errors(true)) { - try { - return $this->validate($content); - } finally { - libxml_use_internal_errors(false); - } - } - - // input validation - if (! is_string($content) || '' === $content) { - throw new \InvalidArgumentException('The content to validate must be a non-empty string'); - } - - // clear previous libxml errors - libxml_clear_errors(); - - // create the DOMDocument object - $dom = new DOMDocument(); - $dom->loadXML($content, LIBXML_NOWARNING); - - // check for errors on load XML - if (false !== $xmlerror = libxml_get_last_error()) { - libxml_clear_errors(); - return $this->registerError('Malformed XML Document: ' . $xmlerror->message); + $this->error = ''; + try { + // create the schemas collection + $schemas = $this->buildSchemas(); + // validate the document against the schema collection + $this->validateWithSchemas($schemas); + } catch (LibXmlException $ex) { + $this->error = $ex->getMessage(); + return false; } - - // create the schemas collection, then validate the document against the schemas - $schemas = $this->buildSchemas($dom); - if ($schemas->count()) { - // build the unique importing schema using the locator - $xsd = $schemas->getXsd($this->locator); - // ask the DOM to validate using the xsd - $dom->schemaValidateSource($xsd); - // check for errors on load XML - if (false !== $xmlerror = libxml_get_last_error()) { - libxml_clear_errors(); - return $this->registerError('Invalid XML Document: ' . $xmlerror->message); - } - } - - $this->registerError(''); return true; } + public function getLastError(): string + { + return $this->error; + } + /** - * Utility function to setup the error property - * Always return FALSE - * @param string $error - * @return false + * Validate against a list of schemas (if any) + * + * @param Schemas $schemas + * @return void + * @throws LibXmlException if schema validation fails */ - private function registerError($error) + public function validateWithSchemas(Schemas $schemas) { - $this->error = trim($error); - return false; + // create the schemas collection, then validate the document against the schemas + if (! $schemas->count()) { + return; + } + // build the unique importing schema + $xsd = $schemas->getImporterXsd(); + $document = $this->document; + LibXmlException::useInternalErrors(function () use ($document, $xsd) { + $document->schemaValidateSource($xsd); + }); } /** * Retrieve a list of namespaces based on the schemaLocation attributes - * @param DOMDocument $dom + * + * @throws SchemaValidatorException if the content of schemaLocation is not an even number of uris * @return Schemas */ - protected function buildSchemas(DOMDocument $dom) + public function buildSchemas(): Schemas { $schemas = new Schemas(); - $xpath = new DOMXPath($dom); + $xpath = new DOMXPath($this->document); + // get the http://www.w3.org/2001/XMLSchema-instance namespace (it could not be 'xsi') - $xsi = $dom->lookupPrefix('http://www.w3.org/2001/XMLSchema-instance'); + $xsi = $this->document->lookupPrefix('http://www.w3.org/2001/XMLSchema-instance'); + // the namespace is not registered, no need to continue if (! $xsi) { return $schemas; } + // get all the xsi:schemaLocation attributes in the document $schemasList = $xpath->query("//@$xsi:schemaLocation"); + // schemaLocation attribute not found, no need to continue if (false === $schemasList || 0 === $schemasList->length) { return $schemas; } - // for every schemaLocation + + // process every schemaLocation for even parts for ($s = 0; $s < $schemasList->length; $s++) { // get the node content $content = $schemasList->item($s)->nodeValue; // get parts without inner spaces $parts = array_values(array_filter(explode(' ', $content))); + $partsCount = count($parts); // check that the list count is an even number - if (0 !== count($parts) % 2) { - throw new \RuntimeException( + if (0 !== $partsCount % 2) { + throw new SchemaValidatorException( "The schemaLocation value '" . $content . "' must have even number of URIs" ); } // insert the uris pairs into the schemas - $partsCount = count($parts); for ($k = 0; $k < $partsCount; $k = $k + 2) { $schemas->create($parts[$k], $parts[$k + 1]); } } + return $schemas; } } diff --git a/src/XmlSchemaValidator/SchemaValidatorException.php b/src/XmlSchemaValidator/SchemaValidatorException.php new file mode 100644 index 0000000..4f7795f --- /dev/null +++ b/src/XmlSchemaValidator/SchemaValidatorException.php @@ -0,0 +1,6 @@ +mimeChecker = new FileMimeChecker([ - 'text/xml' => null, - 'text/plain' => null, - 'application/xml' => null, - ]); - } - /** - * Return a the XML of a Xsd that includes all the namespaces - * @param Locator $locator + * Return the XML of a Xsd that includes all the namespaces + * with the local location + * * @return string */ - public function getXsd(Locator $locator) + public function getImporterXsd(): string { - $lines = []; + $xsd = new \DOMDocument('1.0', 'utf-8'); + $xsd->loadXML(''); foreach ($this->schemas as $schema) { - $file = $locator->get($schema->getLocation()); - if ($this->fileIsXsd($file)) { - $lines[] = ''; - } + $node = $xsd->createElementNS('http://www.w3.org/2001/XMLSchema', 'import'); + $node->setAttribute('namespace', $schema->getNamespace()); + $node->setAttribute('schemaLocation', $schema->getLocation()); + $xsd->documentElement->appendChild($node); } - return '' . "\n" - . '' - . implode('', $lines) - . ''; - } - - /** - * @param $filename - * @return bool - */ - protected function fileIsXsd($filename) - { - return $this->mimeChecker->check($filename); + return $xsd->saveXML(); } /** @@ -63,7 +35,7 @@ protected function fileIsXsd($filename) * @param string $location * @return Schema */ - public function create($namespace, $location) + public function create(string $namespace, string $location): Schema { return $this->insert(new Schema($namespace, $location)); } @@ -74,7 +46,7 @@ public function create($namespace, $location) * @param Schema $schema * @return Schema */ - public function insert(Schema $schema) + public function insert(Schema $schema): Schema { $this->schemas[$schema->getNamespace()] = $schema; return $schema; @@ -84,7 +56,7 @@ public function insert(Schema $schema) * Remove a schema * @param string $namespace */ - public function remove($namespace) + public function remove(string $namespace) { unset($this->schemas[$namespace]); } @@ -93,7 +65,7 @@ public function remove($namespace) * Return the complete collection of schemas as an associative array * @return Schema[] */ - public function all() + public function all(): array { return $this->schemas; } @@ -102,7 +74,7 @@ public function all() * @param string $namespace * @return bool */ - public function exists($namespace) + public function exists(string $namespace): bool { return array_key_exists($namespace, $this->schemas); } @@ -112,7 +84,7 @@ public function exists($namespace) * @param string $namespace * @return Schema */ - public function item($namespace) + public function item(string $namespace): Schema { if (! $this->exists($namespace)) { throw new \InvalidArgumentException("Namespace $namespace does not exists in the schemas"); diff --git a/src/XmlSchemaValidator/SetStrings.php b/src/XmlSchemaValidator/SetStrings.php deleted file mode 100644 index 0175328..0000000 --- a/src/XmlSchemaValidator/SetStrings.php +++ /dev/null @@ -1,83 +0,0 @@ -clear(); - $this->addAll($members); - } - - public function clear() - { - $this->members = []; - } - - public function addAll(array $members) - { - foreach ($members as $member) { - $this->add($member); - } - } - - public function all() - { - return array_keys($this->members); - } - - public function add($member) - { - $member = $this->cast($member); - if ('' === $member) { - return false; - } - if ($this->contains($member)) { - return false; - } - $this->members[$member] = null; - return true; - } - - public function cast($member) - { - if (is_object($member)) { - $member = is_callable([$member, '__toString']) ? (string) $member : ''; - } - return strval($member); - } - - public function contains($member) - { - $member = $this->cast($member); - return array_key_exists($member, $this->members); - } - - public function remove($member) - { - $member = $this->cast($member); - unset($this->members[$member]); - } - - public function getIterator() - { - return new \ArrayIterator($this->all()); - } - - public function count() - { - return count($this->members); - } -} diff --git a/tests/XmlSchemaValidatorTests/Downloader/CurlDownloaderTest.php b/tests/XmlSchemaValidatorTests/Downloader/CurlDownloaderTest.php deleted file mode 100644 index fa21d1d..0000000 --- a/tests/XmlSchemaValidatorTests/Downloader/CurlDownloaderTest.php +++ /dev/null @@ -1,13 +0,0 @@ -downloader = new CurlDownloader(); - } -} diff --git a/tests/XmlSchemaValidatorTests/Downloader/DownloaderTestAbstract.php b/tests/XmlSchemaValidatorTests/Downloader/DownloaderTestAbstract.php deleted file mode 100644 index a8debce..0000000 --- a/tests/XmlSchemaValidatorTests/Downloader/DownloaderTestAbstract.php +++ /dev/null @@ -1,38 +0,0 @@ -utilAssetLocation('cfdv32.xsd'); - $temporary = $this->downloader->download($url, $timeout); - $this->assertFileEquals($expected, $temporary); - } - - public function testDownloaderDownloadFail() - { - $url = 'http://127.0.0.1:8999/non-existent.xsd'; - $timeout = 1; - - $this->expectException(\Exception::class); - $this->downloader->download($url, $timeout); - } - - public function testDownloaderDownloadFailNoServer() - { - $url = 'http://127.0.0.10:8998/non-existent.xsd'; - $timeout = 1; - - $this->expectException(\Exception::class); - $this->downloader->download($url, $timeout); - } -} diff --git a/tests/XmlSchemaValidatorTests/Downloader/FakeDownloader.php b/tests/XmlSchemaValidatorTests/Downloader/FakeDownloader.php deleted file mode 100644 index 5193c04..0000000 --- a/tests/XmlSchemaValidatorTests/Downloader/FakeDownloader.php +++ /dev/null @@ -1,26 +0,0 @@ -resources = $resources; - } - - public function download($url, $timeout) - { - if (! isset($this->resources[$url])) { - throw new \RuntimeException("Download fail for url $url Resource does not exists"); - } - $tempname = tempnam(null, null); - if (! @copy($this->resources[$url], $tempname)) { - throw new \RuntimeException("Download fail for url $url Cannot place on $tempname"); - } - return $tempname; - } -} diff --git a/tests/XmlSchemaValidatorTests/Downloader/NullDownloaderTest.php b/tests/XmlSchemaValidatorTests/Downloader/NullDownloaderTest.php deleted file mode 100644 index 181e11d..0000000 --- a/tests/XmlSchemaValidatorTests/Downloader/NullDownloaderTest.php +++ /dev/null @@ -1,20 +0,0 @@ -downloader = new NullDownloader(); - } - - public function testDownloaderDownloadSuccess() - { - // NullDownloader always fail - $this->expectException(\Exception::class); - parent::testDownloaderDownloadSuccess(); - } -} diff --git a/tests/XmlSchemaValidatorTests/Downloader/PhpDownloaderTest.php b/tests/XmlSchemaValidatorTests/Downloader/PhpDownloaderTest.php deleted file mode 100644 index e82c798..0000000 --- a/tests/XmlSchemaValidatorTests/Downloader/PhpDownloaderTest.php +++ /dev/null @@ -1,13 +0,0 @@ -downloader = new PhpDownloader(); - } -} diff --git a/tests/XmlSchemaValidatorTests/LocatorTest.php b/tests/XmlSchemaValidatorTests/LocatorTest.php deleted file mode 100644 index 964c073..0000000 --- a/tests/XmlSchemaValidatorTests/LocatorTest.php +++ /dev/null @@ -1,246 +0,0 @@ -locator = new Locator('', 20, 0, new Downloader\FakeDownloader([ - 'http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd' - => $this->utilAssetLocation('cfdv32.xsd'), - 'http://www.sat.gob.mx/sitio_internet/cfd/TimbreFiscalDigital/TimbreFiscalDigital.xsd' - => $this->utilAssetLocation('TimbreFiscalDigital.xsd'), - ])); - } - - public function testBuildLocatorDefaultOptions() - { - $locator = new Locator(); - $this->assertSame( - sys_get_temp_dir(), - $this->locator->getRepository(), - 'Default repository expected to be the system temp dir' - ); - $this->assertSame(20, $locator->getTimeout(), 'Default timeout expected to be 20'); - $this->assertSame(0, $locator->getExpire(), 'Default timeout expected to be 0'); - $this->assertInstanceOf(DownloaderInterface::class, $locator->getDownloader()); - } - - public function testBuildLocatorOptionRepository() - { - $loc = new Locator(__DIR__); - $this->assertSame(__DIR__, $loc->getRepository(), 'Repository value expected to be the same as provided'); - } - - public function testBuildLocatorOptionTimeout() - { - $loc = new Locator('', 5); - $this->assertSame(5, $loc->getTimeout(), 'Timeout value expected to be the same as provided'); - } - - public function testBuildLocatorOptionExpires() - { - $loc = new Locator('', 20, 5); - $this->assertSame(5, $loc->getExpire(), 'Expire value expected to be the same as provided'); - } - - public function testBuildLocatorOptionDownloader() - { - $nullDownloader = new NullDownloader(); - $loc = new Locator('', 20, 5, $nullDownloader); - $this->assertSame($nullDownloader, $loc->getDownloader(), 'Downloader object was not the same as provided'); - } - - public function testMimes() - { - $this->assertCount(0, $this->locator->mimeList()); - $this->locator->mimeAllow('text/xml'); - $this->locator->mimeAllow('TEXT/xml'); - $this->locator->mimeAllow('application/xml'); - $this->locator->mimeAllow('image/png'); - $this->assertCount(3, $this->locator->mimeList(), 'Only 3 mimes stored since it is placed in lowercase'); - $this->locator->mimeDisallow('image/png'); - $this->assertCount(2, $this->locator->mimeList(), 'Only 2 mimes stored since one was removed'); - $this->locator->mimeDisallow('some'); - $list = $this->locator->mimeList(); - $this->assertCount(2, $list, 'Only 2 mimes stored since last removed does not exists'); - $this->assertContains('text/xml', $list, 'The list of mime contains text/xml'); - $this->assertContains('application/xml', $list, 'The list of mime contains application/xml'); - } - - public function testRegisterAFileThatExists() - { - $fxml = $this->utilAssetLocation('sample.xml'); - $this->assertFileExists($fxml, 'The file sample.xml for testing must exists'); - $this->locator->register('http://example.com/X1', $fxml); - $this->assertSame($fxml, $this->locator->get('http://example.com/X1'), 'Register a valid file that exists'); - } - - public function testDownloadURL() - { - $fcfdv32 = $this->utilAssetLocation('cfdv32.xsd'); - $this->assertFileExists($fcfdv32, 'The file cfdv32.xsd for testing must exists'); - - $filename = $this->locator->cacheFileName($this->urlCfdiXsd); - if (file_exists($filename)) { - unlink($filename); - } - - $this->assertFileNotExists($filename, 'The cache file for cfdv32.xsd must not exists'); - $this->assertSame( - $filename, - $this->locator->get($this->urlCfdiXsd), - 'Cache file and received cache file from get method are the same' - ); - $this->assertFileExists($filename, 'The cache file for cfdv32.xsd was not downloaded'); - $this->assertFileEquals( - $fcfdv32, - $filename, - 'The cache file for cfdv32.xsd must have the same content as the file downloaded' - ); - } - - public function testDownloadWithExpiration() - { - // locator with expire settings - $locator = new Locator( - $this->locator->getRepository(), - $this->locator->getTimeout(), - 1800, - $this->locator->getDownloader() - ); - - // files to compare - $fcfdv32 = $this->utilAssetLocation('cfdv32.xsd'); - $cfdicache = $locator->cacheFileName($this->urlCfdiXsd); - if (file_exists($cfdicache)) { - unlink($cfdicache); - } - - // download because the file does not exists - $this->assertFileNotExists($cfdicache, 'The file must not exists'); - $locator->get($this->urlCfdiXsd); - $this->assertFileEquals($fcfdv32, $cfdicache, 'Check if the file was since it was deleted'); - - file_put_contents($cfdicache, '--override--'); - touch($cfdicache, time() - 10); // set the mtime to 10 seconds ago - $locator->get($this->urlCfdiXsd); - $this->assertEquals( - '--override--', - file_get_contents($cfdicache), - 'The file did not expires, the content is not the same that we alter' - ); - - // store other content and different the mtime - touch($cfdicache, time() - 3600); // set the mtime to yesterday - $locator->get($this->urlCfdiXsd); - $this->assertFileEquals($fcfdv32, $cfdicache, 'The cache file did not expire, didnt download a fresh copy'); - - // remove file - unlink($cfdicache); - } - - /** - * @depends testDownloadURL - */ - public function testDownloadAndMoveException() - { - $protected = '/sbin'; - if (! is_dir($protected) or is_writable($protected)) { - $this->markTestIncomplete( - "This test expect to find a folder $protected without write permissions," - . ' ¿are you running this on windows or as root?' - ); - } - $locator = new Locator( - $protected, - $this->locator->getTimeout(), - $this->locator->getExpire(), - $this->locator->getDownloader() - ); - $filename = $locator->cacheFileName($this->urlCfdiXsd); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("Cannot move the temporary file to $filename"); - $this->assertSame($filename, $locator->get($this->urlCfdiXsd), 'Return the same name'); - } - - public function testRegisterMaintenance() - { - $expected = [ - 'http://example.com/X1' => $this->utilAssetLocation('sample.xml'), - 'http://example.com/X2' => $this->utilAssetLocation('sample.xml'), - ]; - foreach ($expected as $key => $value) { - $this->locator->register($key, $value); - } - $this->assertSame($expected, $this->locator->registry()); - $this->assertTrue($this->locator->registered('http://example.com/X1'), 'Registered URL must exists'); - $this->assertFalse($this->locator->registered('http://example.com/X3'), 'Not registered URL must not exists'); - $this->locator->unregister('http://example.com/X2'); - $this->assertFalse($this->locator->registered('http://example.com/X2'), 'Unregistered URL must not exists'); - $this->assertCount(1, $this->locator->registry(), 'The final count of the registry must be 1'); - } - - public function testRegisterAnEmptyUrl() - { - $url = ''; - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('Url (empty) is not valid'); - $this->locator->register($url, 'sample.xml'); - } - - public function testRegisterAnInvalidUrl() - { - $url = 'not-an-url'; - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("Url $url is not valid"); - $this->locator->register($url, 'sample.xml'); - } - - public function testRegisterNonExistentFile() - { - $this->locator->mimeAllow('image/png'); // allow only png images - $file = $this->utilAssetLocation('does-not-exists.txt'); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("File $file does not exists or is not readable"); - $this->locator->register('http://example.com/some', $file); - } - - public function testRegisterAnInvalidMime() - { - $this->locator->mimeAllow('image/png'); // allow only png images - $file = $this->utilAssetLocation('sample.xml'); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("File $file is not a valid mime type"); - $this->locator->register('http://example.com/some', $file); - } - - /** - * @depends testDownloadURL - */ - public function testDownloadWithAnInvalidMime() - { - $this->locator->mimeAllow('image/png'); // allow only png images - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("Downloaded file from {$this->urlCfdiXsd} is not a valid mime"); - $this->locator->get($this->urlCfdiXsd); - } - - public function testDownloadWithNonExistentUrl() - { - $url = 'http://example.com/non-exists.htm'; - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage("Download fail for url $url"); - $this->locator->get($url); - } -} diff --git a/tests/XmlSchemaValidatorTests/SchemaValidatorTest.php b/tests/XmlSchemaValidatorTests/SchemaValidatorTest.php index 4de03c5..9b214b1 100644 --- a/tests/XmlSchemaValidatorTests/SchemaValidatorTest.php +++ b/tests/XmlSchemaValidatorTests/SchemaValidatorTest.php @@ -1,129 +1,97 @@ assertInstanceOf(Locator::class, $validator->getLocator(), 'The locator exists and is an object'); - $this->assertEmpty($validator->getError(), 'There are no errors'); - } - - public function providerValidateInvalidArgumentException() - { - return [ - [''], // empty string is also invalid - [null], - [new \stdClass()], - [[]], - [false], - [0], - ]; + $location = $this->utilAssetLocation($file); + if (! file_exists($location)) { + $this->markTestSkipped("The file $location was not found"); + } + $content = file_get_contents($location); + return new SchemaValidator($content); } - /** - * @dataProvider providerValidateInvalidArgumentException - * @param mixed $badargument - */ - public function testValidateInvalidArgumentException($badargument) + public function testConstructorWithEmptyString() { - $validator = new SchemaValidator(); $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('The content to validate must be a non-empty string'); - $validator->validate($badargument); + $this->expectExceptionMessage('empty'); + new SchemaValidator(''); } public function testValidatePreserveLibXmlErrors() { libxml_use_internal_errors(false); - $validator = new SchemaValidator(); - $this->assertFalse($validator->validate(' ')); + $this->expectException(SchemaValidatorException::class); + $this->expectExceptionMessage("Malformed XML Document: Start tag expected, '<' not found"); + new SchemaValidator(' this is not a valid xml '); $this->assertFalse(libxml_use_internal_errors()); - $this->assertSame("Malformed XML Document: Start tag expected, '<' not found", $validator->getError()); } public function testValidateWithNoSchema() { - $sample = $this->utilAssetLocation('sample.xml'); - $this->assertFileExists($sample, 'Must exists files/sample.xml'); - $validator = new SchemaValidator(); + $validator = $this->utilCreateValidator('xml-without-schemas.xml'); $this->assertTrue( - $validator->validate(file_get_contents($sample)), - 'Validation without schemas and well formed document return true' + $validator->validate(), + 'Validation without schemas and well formed document must return true' ); } public function testValidateWithNotListedSchemaLocations() { - $sample = $this->utilAssetLocation('not-listed-schemalocations.xml'); - $this->assertFileExists($sample, 'Must exists test file not-even-schemalocations.xml'); - $validator = new SchemaValidator(); - $this->assertTrue($validator->validate(file_get_contents($sample))); + $validator = $this->utilCreateValidator('not-listed-schemalocations.xml'); + $this->assertTrue($validator->validate()); } public function testValidateWithNotEvenSchemaLocations() { - $sample = $this->utilAssetLocation('not-even-schemalocations.xml'); - $this->assertFileExists($sample, 'Must exists test file not-even-schemalocations.xml'); - $validator = new SchemaValidator(); - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessageRegExp( - '/The schemaLocation value \'.*\' must have even number of URIs/' - ); - $validator->validate(file_get_contents($sample)); + $validator = $this->utilCreateValidator('not-even-schemalocations.xml'); + + $this->expectException(SchemaValidatorException::class); + $this->expectExceptionMessage('must have even number of URIs'); + $validator->validate(); } - /** - * @param bool $withCommonXsd - * @return Locator - */ - private function buildLocator($withCommonXsd) + public function testValidateValidXmlWithSchema() { - $locator = new Locator(); - $locator->mimeAllow('text/xml'); - $locator->mimeAllow('application/xml'); - if ($withCommonXsd) { - $locator->register( - 'http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd', - $this->utilAssetLocation('cfdv32.xsd') - ); - $locator->register( - 'http://www.sat.gob.mx/TimbreFiscalDigital/TimbreFiscalDigital.xsd', - $this->utilAssetLocation('TimbreFiscalDigital.xsd') - ); - } - return $locator; + $validator = $this->utilCreateValidator('books-valid.xml'); + + $this->assertTrue($validator->validate()); } - public function testValidateValidCFDIWithoutDownload() + public function testValidateValidXmlWithTwoSchemas() { - $cfdifile = $this->utilAssetLocation('cfdi-valid-minimal.xml'); - $this->assertFileExists($cfdifile, 'Must exists files/cfdi-valid-minimal.xml'); - $locator = $this->buildLocator(true); - $validator = new SchemaValidator($locator); - $isValid = $validator->validate(file_get_contents($cfdifile)); - $this->assertTrue( - $isValid, - 'CFDI File is not valid, perhaps the cfdi-valid-minimal.xml contains additional namespaces' - ); - $this->assertEmpty($validator->getError()); + $validator = $this->utilCreateValidator('ticket-valid.xml'); + + $this->assertTrue($validator->validate()); + $this->assertEmpty($validator->getLastError()); + } + + public function testValidateInvalidXmlOnlyOneSchema() + { + $validator = $this->utilCreateValidator('books-invalid.xml'); + + $this->assertFalse($validator->validate()); + $this->assertContains("The attribute 'serie' is required but missing", $validator->getLastError()); } - public function testValidateInValidCFDIWithoutDownload() + public function testValidateInvalidXmlFirstSchemas() { - $cfdifile = $this->utilAssetLocation('cfdi-invalid.xml'); - $this->assertFileExists($cfdifile, 'Must exists files/cfdi-invalid.xml'); - $locator = $this->buildLocator(true); - $validator = new SchemaValidator($locator); - $this->assertFalse($validator->validate(file_get_contents($cfdifile)), 'CFDI File must not be valid'); - $error = $validator->getError(); - $this->assertContains('Invalid XML Document', $error, 'Report Invalid XML Document'); - $this->assertContains('This element is not expected', $error, 'This element is not expected'); - $this->assertContains('{http://www.sat.gob.mx/cfd/3}emisor', $error, 'Mention emisor'); - $this->assertContains('{http://www.sat.gob.mx/cfd/3}Emisor', $error, 'Mention Emisor'); + $validator = $this->utilCreateValidator('ticket-invalid-ticket.xml'); + + $this->assertFalse($validator->validate()); + $this->assertContains("The attribute 'notes' is required but missing", $validator->getLastError()); + } + + public function testValidateInvalidXmlSecondSchemas() + { + $validator = $this->utilCreateValidator('ticket-invalid-book.xml'); + + $this->assertFalse($validator->validate()); + $this->assertContains("The attribute 'serie' is required but missing", $validator->getLastError()); } } diff --git a/tests/XmlSchemaValidatorTests/SchemasTest.php b/tests/XmlSchemaValidatorTests/SchemasTest.php index d83dd28..9494ef4 100644 --- a/tests/XmlSchemaValidatorTests/SchemasTest.php +++ b/tests/XmlSchemaValidatorTests/SchemasTest.php @@ -1,7 +1,6 @@ insert(new Schema($ns, $location)); - $this->assertInstanceOf('\XmlSchemaValidator\Schema', $schema, 'The insert method must return a Schema object'); + $this->assertInstanceOf(Schema::class, $schema, 'The insert method must return a Schema object'); $this->assertCount(1, $schemas); } @@ -88,30 +87,19 @@ public function testRemove() $this->assertCount(5, $schemas, 'Remove a non existent schema do nothing'); } - public function testGetXsdEmpty() + public function testGetImporterXsdEmpty() { $basefile = $this->utilAssetLocation('include-template.xsd'); $this->assertFileExists($basefile, "File $basefile must exists"); $schemas = new Schemas(); - $filename = tempnam(null, null); - file_put_contents($filename, $schemas->getXsd(new Locator())); - $this->assertXmlFileEqualsXmlFile($basefile, $filename, 'Empty Xsd must match files/include-template.xsd'); - unlink($filename); + $this->assertXmlStringEqualsXmlFile($basefile, $schemas->getImporterXsd()); } - public function testGetXsdWithContents() + public function testGetImporterXsdWithContents() { - $basefile = $this->utilAssetLocation('include-commonxsd.xsd'); + $basefile = $this->utilAssetLocation('include-realurls.xsd'); $this->assertFileExists($basefile, "File $basefile must exists"); - $commonxsdfolder = $this->utilAssetLocation(''); - $fcfdv32 = $this->utilAssetLocation('cfdv32.xsd'); - $this->assertFileExists($fcfdv32, "The file $fcfdv32 for testing must exists"); - $ftimbre = $this->utilAssetLocation('TimbreFiscalDigital.xsd'); - $this->assertFileExists($fcfdv32, "The file $ftimbre for testing must exists"); - $locator = new Locator(); - $locator->register('http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd', $fcfdv32); - $locator->register('http://www.sat.gob.mx/TimbreFiscalDigital/TimbreFiscalDigital.xsd', $ftimbre); $schemas = new Schemas(); $schemas->create( 'http://www.sat.gob.mx/cfd/3', @@ -121,21 +109,8 @@ public function testGetXsdWithContents() 'http://www.sat.gob.mx/TimbreFiscalDigital', 'http://www.sat.gob.mx/TimbreFiscalDigital/TimbreFiscalDigital.xsd' ); - $filename = tempnam(null, null); - // verify that the Xsd contains the location of the commonXsd folder - $xsdcontents = $schemas->getXsd($locator); - $this->assertContains( - ' schemaLocation="' . $commonxsdfolder, - $xsdcontents, - "The returned Xsd must contain the absolute path to $commonxsdfolder" - ); - // change the default XSD contents because it contains absolute paths, replace with a constant before compare - file_put_contents($filename, str_replace($commonxsdfolder, '__COMMONXSDPATH__/', $schemas->getXsd($locator))); - $this->assertXmlFileEqualsXmlFile( - $basefile, - $filename, - 'SAT simple include schema must match include-template.xsd' - ); + + $this->assertXmlStringEqualsXmlFile($basefile, $schemas->getImporterXsd()); } public function testIteratorAggregate() diff --git a/tests/XmlSchemaValidatorTests/TestCase.php b/tests/XmlSchemaValidatorTests/TestCase.php index b17ba5d..5bbed9c 100644 --- a/tests/XmlSchemaValidatorTests/TestCase.php +++ b/tests/XmlSchemaValidatorTests/TestCase.php @@ -11,6 +11,6 @@ class TestCase extends \PHPUnit\Framework\TestCase */ protected function utilAssetLocation($filename) { - return __DIR__ . '/../assets/' . $filename; + return dirname(__DIR__) . '/assets/' . $filename; } } diff --git a/tests/assets/TimbreFiscalDigital.xsd b/tests/assets/TimbreFiscalDigital.xsd deleted file mode 100644 index 89065dc..0000000 --- a/tests/assets/TimbreFiscalDigital.xsd +++ /dev/null @@ -1,68 +0,0 @@ - - - - - Complemento requerido para el Timbrado Fiscal Digital que da valides a un Comprobante Fiscal Digital. - - - - - Atributo requerido para la expresión de la versión del estándar del Timbre Fiscal Digital - - - - - Atributo requerido para expresar los 36 caracteres del UUID de la transacción de timbrado - - - - - - - - - - - - Atributo requerido para expresar la fecha y hora de la generación del timbre - - - - - - - - - - Atributo requerido para contener el sello digital del comprobante fiscal, que será timbrado. El sello deberá ser expresado cómo una cadena de texto en formato Base 64. - - - - - - - - - - Atributo requerido para expresar el número de serie del certificado del SAT usado para el Timbre - - - - - - - - - - - Atributo requerido para contener el sello digital del Timbre Fiscal Digital, al que hacen referencia las reglas de resolución miscelánea aplicable. El sello deberá ser expresado cómo una cadena de texto en formato Base 64. - - - - - - - - - - diff --git a/tests/assets/books-invalid.xml b/tests/assets/books-invalid.xml new file mode 100644 index 0000000..93113a0 --- /dev/null +++ b/tests/assets/books-invalid.xml @@ -0,0 +1,14 @@ + + + + My first Example + John Doe + Foo Bar + + + My secondExample + John Doe + Foo Bar + + diff --git a/tests/assets/books-valid.xml b/tests/assets/books-valid.xml new file mode 100644 index 0000000..3f41707 --- /dev/null +++ b/tests/assets/books-valid.xml @@ -0,0 +1,14 @@ + + + + My first Example + John Doe + Foo Bar + + + My secondExample + John Doe + Foo Bar + + diff --git a/tests/assets/cfdi-invalid.xml b/tests/assets/cfdi-invalid.xml deleted file mode 100644 index ed07aaf..0000000 --- a/tests/assets/cfdi-invalid.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/assets/cfdi-valid-minimal.xml b/tests/assets/cfdi-valid-minimal.xml deleted file mode 100644 index 0343ff8..0000000 --- a/tests/assets/cfdi-valid-minimal.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/tests/assets/cfdv32.xsd b/tests/assets/cfdv32.xsd deleted file mode 100644 index e0da312..0000000 --- a/tests/assets/cfdv32.xsd +++ /dev/null @@ -1,903 +0,0 @@ - - - - - - Estándar de Comprobante fiscal digital a través de Internet. - - - - - - Nodo requerido para expresar la información del contribuyente emisor del comprobante. - - - - - - Nodo opcional para precisar la información de ubicación del domicilio fiscal del contribuyente emisor - - - - - Nodo opcional para precisar la información de ubicación del domicilio en donde es emitido el comprobante fiscal en caso de que sea distinto del domicilio fiscal del contribuyente emisor. - - - - - - Nodo requerido para incorporar los regímenes en los que tributa el contribuyente emisor. Puede contener más de un régimen. - - - - - Atributo requerido para incorporar el nombre del régimen en el que tributa el contribuyente emisor. - - - - - - - - - - - - - - - Atributo requerido para la Clave del Registro Federal de Contribuyentes correspondiente al contribuyente emisor del comprobante sin guiones o espacios. - - - - - Atributo opcional para el nombre, denominación o razón social del contribuyente emisor del comprobante. - - - - - - - - - - - - - Nodo requerido para precisar la información del contribuyente receptor del comprobante. - - - - - - Nodo opcional para la definición de la ubicación donde se da el domicilio del receptor del comprobante fiscal. - - - - - - Atributo requerido para precisar la Clave del Registro Federal de Contribuyentes correspondiente al contribuyente receptor del comprobante. - - - - - Atributo opcional para el nombre, denominación o razón social del contribuyente receptor del comprobante. - - - - - - - - - - - - - Nodo requerido para enlistar los conceptos cubiertos por el comprobante. - - - - - - Nodo para introducir la información detallada de un bien o servicio amparado en el comprobante. - - - - - - Nodo opcional para introducir la información aduanera aplicable cuando se trate de ventas de primera mano de mercancías importadas. - - - - - Nodo opcional para asentar el número de cuenta predial con el que fue registrado el inmueble, en el sistema catastral de la entidad federativa de que trate, o bien para incorporar los datos de identificación del certificado de participación inmobiliaria no amortizable. - - - - - Atributo requerido para precisar el número de la cuenta predial del inmueble cubierto por el presente concepto, o bien para incorporar los datos de identificación del certificado de participación inmobiliaria no amortizable, tratándose de arrendamiento. - - - - - - - - - - - - - Nodo opcional donde se incluirán los nodos complementarios de extensión al concepto, definidos por el SAT, de acuerdo a disposiciones particulares a un sector o actividad especifica. - - - - - - - - - - Nodo opcional para expresar las partes o componentes que integran la totalidad del concepto expresado en el comprobante fiscal digital a través de Internet - - - - - - Nodo opcional para introducir la información aduanera aplicable cuando se trate de partes o componentes importados vendidos de primera mano. - - - - - - Atributo requerido para precisar la cantidad de bienes o servicios del tipo particular definido por la presente parte. - - - - - - - - - - Atributo opcional para precisar la unidad de medida aplicable para la cantidad expresada en la parte. - - - - - - - - - - - Atributo opcional para expresar el número de serie del bien o identificador del servicio amparado por la presente parte. - - - - - - - - - - - Atributo requerido para precisar la descripción del bien o servicio cubierto por la presente parte. - - - - - - - - - - - Atributo opcional para precisar el valor o precio unitario del bien o servicio cubierto por la presente parte. - - - - - Atributo opcional para precisar el importe total de los bienes o servicios de la presente parte. Debe ser equivalente al resultado de multiplicar la cantidad por el valor unitario expresado en la parte. - - - - - - - - Atributo requerido para precisar la cantidad de bienes o servicios del tipo particular definido por el presente concepto. - - - - - - - - - - Atributo requerido para precisar la unidad de medida aplicable para la cantidad expresada en el concepto. - - - - - - - - - - - Atributo opcional para expresar el número de serie del bien o identificador del servicio amparado por el presente concepto. - - - - - - - - - - - Atributo requerido para precisar la descripción del bien o servicio cubierto por el presente concepto. - - - - - - - - - - - Atributo requerido para precisar el valor o precio unitario del bien o servicio cubierto por el presente concepto. - - - - - Atributo requerido para precisar el importe total de los bienes o servicios del presente concepto. Debe ser equivalente al resultado de multiplicar la cantidad por el valor unitario expresado en el concepto. - - - - - - - - - - Nodo requerido para capturar los impuestos aplicables. - - - - - - Nodo opcional para capturar los impuestos retenidos aplicables - - - - - - Nodo para la información detallada de una retención de impuesto específico - - - - - Atributo requerido para señalar el tipo de impuesto retenido - - - - - - - Impuesto sobre la renta - - - - - Impuesto al Valor Agregado - - - - - - - - Atributo requerido para señalar el importe o monto del impuesto retenido - - - - - - - - - - Nodo opcional para asentar o referir los impuestos trasladados aplicables - - - - - - Nodo para la información detallada de un traslado de impuesto específico - - - - - Atributo requerido para señalar el tipo de impuesto trasladado - - - - - - - Impuesto al Valor Agregado - - - - - Impuesto especial sobre productos y servicios - - - - - - - - Atributo requerido para señalar la tasa del impuesto que se traslada por cada concepto amparado en el comprobante - - - - - Atributo requerido para señalar el importe del impuesto trasladado - - - - - - - - - - - Atributo opcional para expresar el total de los impuestos retenidos que se desprenden de los conceptos expresados en el comprobante fiscal digital a través de Internet. - - - - - Atributo opcional para expresar el total de los impuestos trasladados que se desprenden de los conceptos expresados en el comprobante fiscal digital a través de Internet. - - - - - - - Nodo opcional donde se incluirá el complemento Timbre Fiscal Digital de manera obligatoria y los nodos complementarios determinados por el SAT, de acuerdo a las disposiciones particulares a un sector o actividad específica. - - - - - - - - - - Nodo opcional para recibir las extensiones al presente formato que sean de utilidad al contribuyente. Para las reglas de uso del mismo, referirse al formato de origen. - - - - - - - - - - - Atributo requerido con valor prefijado a 3.2 que indica la versión del estándar bajo el que se encuentra expresado el comprobante. - - - - - - - - - - Atributo opcional para precisar la serie para control interno del contribuyente. Este atributo acepta una cadena de caracteres alfabéticos de 1 a 25 caracteres sin incluir caracteres acentuados. - - - - - - - - - - - - Atributo opcional para control interno del contribuyente que acepta un valor numérico entero superior a 0 que expresa el folio del comprobante. - - - - - - - - - - - - Atributo requerido para la expresión de la fecha y hora de expedición del comprobante fiscal. Se expresa en la forma aaaa-mm-ddThh:mm:ss, de acuerdo con la especificación ISO 8601. - - - - - - - - - - Atributo requerido para contener el sello digital del comprobante fiscal, al que hacen referencia las reglas de resolución miscelánea aplicable. El sello deberá ser expresado cómo una cadena de texto en formato Base 64. - - - - - - - - - - Atributo requerido para precisar la forma de pago que aplica para este comprobnante fiscal digital a través de Internet. Se utiliza para expresar Pago en una sola exhibición o número de parcialidad pagada contra el total de parcialidades, Parcialidad 1 de X. - - - - - - - - - - Atributo requerido para expresar el número de serie del certificado de sello digital que ampara al comprobante, de acuerdo al acuse correspondiente a 20 posiciones otorgado por el sistema del SAT. - - - - - - - - - - - Atributo requerido que sirve para expresar el certificado de sello digital que ampara al comprobante como texto, en formato base 64. - - - - - - - - - - Atributo opcional para expresar las condiciones comerciales aplicables para el pago del comprobante fiscal digital a través de Internet. - - - - - - - - - - - Atributo requerido para representar la suma de los importes antes de descuentos e impuestos. - - - - - Atributo opcional para representar el importe total de los descuentos aplicables antes de impuestos. - - - - - Atributo opcional para expresar el motivo del descuento aplicable. - - - - - - - - - - - Atributo opcional para representar el tipo de cambio conforme a la moneda usada - - - - - - - - - - Atributo opcional para expresar la moneda utilizada para expresar los montos - - - - - - - - - - Atributo requerido para representar la suma del subtotal, menos los descuentos aplicables, más los impuestos trasladados, menos los impuestos retenidos. - - - - - Atributo requerido para expresar el efecto del comprobante fiscal para el contribuyente emisor. - - - - - - - - - - - - Atributo requerido de texto libre para expresar el método de pago de los bienes o servicios amparados por el comprobante. Se entiende como método de pago leyendas tales como: cheque, tarjeta de crédito o debito, depósito en cuenta, etc. - - - - - - - - - - - Atributo requerido para incorporar el lugar de expedición del comprobante. - - - - - - - - - - - Atributo Opcional para incorporar al menos los cuatro últimos digitos del número de cuenta con la que se realizó el pago. - - - - - - - - - - - Atributo opcional para señalar el número de folio fiscal del comprobante que se hubiese expedido por el valor total del comprobante, tratándose del pago en parcialidades. - - - - - - - - - - Atributo opcional para señalar la serie del folio del comprobante que se hubiese expedido por el valor total del comprobante, tratándose del pago en parcialidades. - - - - - - - - - - Atributo opcional para señalar la fecha de expedición del comprobante que se hubiese emitido por el valor total del comprobante, tratándose del pago en parcialidades. Se expresa en la forma aaaa-mm-ddThh:mm:ss, de acuerdo con la especificación ISO 8601. - - - - - - - - - - Atributo opcional para señalar el total del comprobante que se hubiese expedido por el valor total de la operación, tratándose del pago en parcialidades - - - - - - - Tipo definido para expresar domicilios o direcciones - - - - Este atributo opcional sirve para precisar la avenida, calle, camino o carretera donde se da la ubicación. - - - - - - - - - - - Este atributo opcional sirve para expresar el número particular en donde se da la ubicación sobre una calle dada. - - - - - - - - - - - Este atributo opcional sirve para expresar información adicional para especificar la ubicación cuando calle y número exterior (noExterior) no resulten suficientes para determinar la ubicación de forma precisa. - - - - - - - - - - - Este atributo opcional sirve para precisar la colonia en donde se da la ubicación cuando se desea ser más específico en casos de ubicaciones urbanas. - - - - - - - - - - - Atributo opcional que sirve para precisar la ciudad o población donde se da la ubicación. - - - - - - - - - - - Atributo opcional para expresar una referencia de ubicación adicional. - - - - - - - - - - - Atributo opcional que sirve para precisar el municipio o delegación (en el caso del Distrito Federal) en donde se da la ubicación. - - - - - - - - - - - Atributo opcional que sirve para precisar el estado o entidad federativa donde se da la ubicación. - - - - - - - - - - - Atributo requerido que sirve para precisar el país donde se da la ubicación. - - - - - - - - - - - Atributo opcional que sirve para asentar el código postal en donde se da la ubicación. - - - - - - - - - - - Tipo definido para expresar domicilios o direcciones - - - - Este atributo requerido sirve para precisar la avenida, calle, camino o carretera donde se da la ubicación. - - - - - - - - - - - Este atributo opcional sirve para expresar el número particular en donde se da la ubicación sobre una calle dada. - - - - - - - - - - - Este atributo opcional sirve para expresar información adicional para especificar la ubicación cuando calle y número exterior (noExterior) no resulten suficientes para determinar la ubicación de forma precisa. - - - - - - - - - - - Este atributo opcional sirve para precisar la colonia en donde se da la ubicación cuando se desea ser más específico en casos de ubicaciones urbanas. - - - - - - - - - - - Atributo opcional que sirve para precisar la ciudad o población donde se da la ubicación. - - - - - - - - - - - Atributo opcional para expresar una referencia de ubicación adicional. - - - - - - - - - - - Atributo requerido que sirve para precisar el municipio o delegación (en el caso del Distrito Federal) en donde se da la ubicación. - - - - - - - - - - - Atributo requerido que sirve para precisar el estado o entidad federativa donde se da la ubicación. - - - - - - - - - - - Atributo requerido que sirve para precisar el país donde se da la ubicación. - - - - - - - - - - - Atributo requerido que sirve para asentar el código postal en donde se da la ubicación. - - - - - - - - - - - - Tipo definido para expresar claves del Registro Federal de Contribuyentes - - - - - - - - - - - Tipo definido para expresar importes numéricos con fracción hasta seis decimales - - - - - - - - - Tipo definido para expresar información aduanera - - - - Atributo requerido para expresar el número del documento aduanero que ampara la importación del bien. - - - - - - - - - - - Atributo requerido para expresar la fecha de expedición del documento aduanero que ampara la importación del bien. Se expresa en el formato aaaa-mm-dd - - - - - - - - - - Atributo opcional para precisar el nombre de la aduana por la que se efectuó la importación del bien. - - - - - - - - - - diff --git a/tests/assets/include-commonxsd.xsd b/tests/assets/include-commonxsd.xsd deleted file mode 100644 index 2b1f875..0000000 --- a/tests/assets/include-commonxsd.xsd +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/tests/assets/include-realurls.xsd b/tests/assets/include-realurls.xsd new file mode 100644 index 0000000..74ac95e --- /dev/null +++ b/tests/assets/include-realurls.xsd @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/assets/ticket-invalid-book.xml b/tests/assets/ticket-invalid-book.xml new file mode 100644 index 0000000..cc4c77e --- /dev/null +++ b/tests/assets/ticket-invalid-book.xml @@ -0,0 +1,15 @@ + + + + + + + My first Example + John Doe + Foo Bar + + + + diff --git a/tests/assets/ticket-invalid-ticket.xml b/tests/assets/ticket-invalid-ticket.xml new file mode 100644 index 0000000..a9478e8 --- /dev/null +++ b/tests/assets/ticket-invalid-ticket.xml @@ -0,0 +1,15 @@ + + + + + + + My first Example + John Doe + Foo Bar + + + + diff --git a/tests/assets/ticket-valid.xml b/tests/assets/ticket-valid.xml new file mode 100644 index 0000000..cb682d1 --- /dev/null +++ b/tests/assets/ticket-valid.xml @@ -0,0 +1,15 @@ + + + + + + + My first Example + John Doe + Foo Bar + + + + diff --git a/tests/assets/sample.xml b/tests/assets/xml-without-schemas.xml similarity index 100% rename from tests/assets/sample.xml rename to tests/assets/xml-without-schemas.xml diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 59244df..62e4133 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -12,7 +12,7 @@ 'php -S %s:%d -t %s >/dev/null 2>&1 & echo $!', '127.0.0.1', '8999', - escapeshellarg(__DIR__ . '/assets') + escapeshellarg(__DIR__ . '/public') ); // Execute the command and store the process ID diff --git a/tests/public/xsd/books.xsd b/tests/public/xsd/books.xsd new file mode 100644 index 0000000..ef95f02 --- /dev/null +++ b/tests/public/xsd/books.xsd @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/tests/public/xsd/ticket.xsd b/tests/public/xsd/ticket.xsd new file mode 100644 index 0000000..9b31c2c --- /dev/null +++ b/tests/public/xsd/ticket.xsd @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + +