diff --git a/Classes/Domain/Import/Importer/FetchData.php b/Classes/Domain/Import/Importer/FetchData.php index 35aed36..0618f35 100644 --- a/Classes/Domain/Import/Importer/FetchData.php +++ b/Classes/Domain/Import/Importer/FetchData.php @@ -25,7 +25,10 @@ namespace WerkraumMedia\ThueCat\Domain\Import\Importer; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface as CacheFrontendInterface; +use WerkraumMedia\ThueCat\Domain\Import\Importer\FetchData\InvalidResponseException; class FetchData { @@ -44,6 +47,16 @@ class FetchData */ private $cache; + /** + * @var string + */ + private $databaseUrlPrefix = 'https://cdb.thuecat.org'; + + /** + * @var string + */ + private $urlPrefix = 'https://thuecat.org'; + public function __construct( RequestFactoryInterface $requestFactory, ClientInterface $httpClient, @@ -54,6 +67,15 @@ class FetchData $this->cache = $cache; } + public function updatedNodes(string $scopeId): array + { + return $this->jsonLDFromUrl( + $this->databaseUrlPrefix + . '/api/ext-sync/get-updated-nodes?syncScopeId=' + . urlencode($scopeId) + ); + } + public function jsonLDFromUrl(string $url): array { $cacheIdentifier = sha1($url); @@ -65,6 +87,8 @@ class FetchData $request = $this->requestFactory->createRequest('GET', $url); $response = $this->httpClient->sendRequest($request); + $this->handleInvalidResponse($response, $request); + $jsonLD = json_decode((string) $response->getBody(), true); if (is_array($jsonLD)) { $this->cache->set($cacheIdentifier, $jsonLD); @@ -73,4 +97,35 @@ class FetchData return []; } + + public function getResourceEndpoint(): string + { + return $this->urlPrefix . '/resources/'; + } + + private function handleInvalidResponse( + ResponseInterface $response, + RequestInterface $request + ): void { + if ($response->getStatusCode() === 200) { + return; + } + + if ($response->getStatusCode() === 401) { + throw new InvalidResponseException( + 'Unauthorized API request, ensure apiKey is properly configured.', + 1622461709 + ); + } + + if ($response->getStatusCode() === 404) { + throw new InvalidResponseException( + sprintf( + 'Not found, given resource could not be found: "%s".', + $request->getUri() + ), + 1622461820 + ); + } + } } diff --git a/Classes/Domain/Import/Importer/FetchData/InvalidResponseException.php b/Classes/Domain/Import/Importer/FetchData/InvalidResponseException.php new file mode 100644 index 0000000..f9f4588 --- /dev/null +++ b/Classes/Domain/Import/Importer/FetchData/InvalidResponseException.php @@ -0,0 +1,26 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +class InvalidResponseException extends \RuntimeException +{ +} diff --git a/Classes/Domain/Import/RequestFactory.php b/Classes/Domain/Import/RequestFactory.php index a2773f0..2a6d3c8 100644 --- a/Classes/Domain/Import/RequestFactory.php +++ b/Classes/Domain/Import/RequestFactory.php @@ -24,17 +24,41 @@ namespace WerkraumMedia\ThueCat\Domain\Import; */ use Psr\Http\Message\RequestInterface; +use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Http\RequestFactory as Typo3RequestFactory; use TYPO3\CMS\Core\Http\Uri; class RequestFactory extends Typo3RequestFactory { + /** + * @var ExtensionConfiguration + */ + private $extensionConfiguration; + + public function __construct( + ExtensionConfiguration $extensionConfiguration + ) { + $this->extensionConfiguration = $extensionConfiguration; + } + public function createRequest(string $method, $uri): RequestInterface { $uri = new Uri((string) $uri); - $uri = $uri->withQuery('?format=jsonld'); - // TODO: Add api key from site + $query = []; + parse_str($uri->getQuery(), $query); + $query = array_merge($query, [ + 'format' => 'jsonld', + ]); + + try { + $query['api_key'] = $this->extensionConfiguration->get('thuecat', 'apiKey'); + } catch (ExtensionConfigurationExtensionNotConfiguredException $e) { + // Nothing todo, not configured, don't add. + } + + $uri = $uri->withQuery(http_build_query($query)); return parent::createRequest($method, $uri); } diff --git a/Classes/Domain/Import/UrlProvider/StaticUrlProvider.php b/Classes/Domain/Import/UrlProvider/StaticUrlProvider.php index 60eb5ae..165ecfc 100644 --- a/Classes/Domain/Import/UrlProvider/StaticUrlProvider.php +++ b/Classes/Domain/Import/UrlProvider/StaticUrlProvider.php @@ -23,7 +23,6 @@ namespace WerkraumMedia\ThueCat\Domain\Import\UrlProvider; * 02110-1301, USA. */ -use TYPO3\CMS\Core\Utility\GeneralUtility; use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportConfiguration; class StaticUrlProvider implements UrlProvider @@ -33,14 +32,6 @@ class StaticUrlProvider implements UrlProvider */ private $urls = []; - public function __construct( - ImportConfiguration $configuration - ) { - if ($configuration instanceof ImportConfiguration) { - $this->urls = $configuration->getUrls(); - } - } - public function canProvideForConfiguration( ImportConfiguration $configuration ): bool { @@ -50,7 +41,10 @@ class StaticUrlProvider implements UrlProvider public function createWithConfiguration( ImportConfiguration $configuration ): UrlProvider { - return GeneralUtility::makeInstance(self::class, $configuration); + $instance = clone $this; + $instance->urls = $configuration->getUrls(); + + return $instance; } public function getUrls(): array diff --git a/Classes/Domain/Import/UrlProvider/SyncScopeUrlProvider.php b/Classes/Domain/Import/UrlProvider/SyncScopeUrlProvider.php new file mode 100644 index 0000000..c6739a4 --- /dev/null +++ b/Classes/Domain/Import/UrlProvider/SyncScopeUrlProvider.php @@ -0,0 +1,74 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use WerkraumMedia\ThueCat\Domain\Import\Importer\FetchData; +use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportConfiguration; + +class SyncScopeUrlProvider implements UrlProvider +{ + /** + * @var FetchData + */ + private $fetchData; + + /** + * @var string + */ + private $syncScopeId = ''; + + public function __construct( + FetchData $fetchData + ) { + $this->fetchData = $fetchData; + } + + public function canProvideForConfiguration( + ImportConfiguration $configuration + ): bool { + return $configuration->getType() === 'syncScope'; + } + + public function createWithConfiguration( + ImportConfiguration $configuration + ): UrlProvider { + $instance = clone $this; + $instance->syncScopeId = $configuration->getSyncScopeId(); + + return $instance; + } + + public function getUrls(): array + { + $response = $this->fetchData->updatedNodes($this->syncScopeId); + + $resourceIds = array_values($response['data']['createdOrUpdated'] ?? []); + + $urls = array_map(function (string $id) { + return $this->fetchData->getResourceEndpoint() . $id; + }, $resourceIds); + + return $urls; + } +} diff --git a/Classes/Domain/Model/Backend/ImportConfiguration.php b/Classes/Domain/Model/Backend/ImportConfiguration.php index 400ab1b..cda9083 100644 --- a/Classes/Domain/Model/Backend/ImportConfiguration.php +++ b/Classes/Domain/Model/Backend/ImportConfiguration.php @@ -97,14 +97,46 @@ class ImportConfiguration extends AbstractEntity return ArrayUtility::getValueByPath($urlEntry, 'url/el/url/vDEF'); }, $this->getEntries()); + $entries = array_filter($entries); + return array_values($entries); } + public function getSyncScopeId(): string + { + if ($this->configuration === '') { + return ''; + } + + $configurationAsArray = $this->getConfigurationAsArray(); + $arrayPath = 'data/sDEF/lDEF/syncScopeId/vDEF'; + + if (ArrayUtility::isValidPath($configurationAsArray, $arrayPath) === false) { + return ''; + } + + return ArrayUtility::getValueByPath( + $configurationAsArray, + $arrayPath + ); + } + private function getEntries(): array { + $configurationAsArray = $this->getConfigurationAsArray(); + + if (ArrayUtility::isValidPath($configurationAsArray, 'data/sDEF/lDEF/urls/el') === false) { + return []; + } + return ArrayUtility::getValueByPath( - GeneralUtility::xml2array($this->configuration), + $configurationAsArray, 'data/sDEF/lDEF/urls/el' ); } + + private function getConfigurationAsArray(): array + { + return GeneralUtility::xml2array($this->configuration); + } } diff --git a/Configuration/FlexForm/ImportConfiguration/SyncScope.xml b/Configuration/FlexForm/ImportConfiguration/SyncScope.xml new file mode 100644 index 0000000..59e715c --- /dev/null +++ b/Configuration/FlexForm/ImportConfiguration/SyncScope.xml @@ -0,0 +1,35 @@ + + + 1 + + + + + + LLL:EXT:thuecat/Resources/Private/Language/locallang_flexform.xlf:importConfiguration.syncScope.sheetTitle + + array + + + + + + input + int,required + + + + + + + + input + trim,required + + + + + + + + diff --git a/Configuration/TCA/tx_thuecat_import_configuration.php b/Configuration/TCA/tx_thuecat_import_configuration.php index d379b94..d001748 100644 --- a/Configuration/TCA/tx_thuecat_import_configuration.php +++ b/Configuration/TCA/tx_thuecat_import_configuration.php @@ -10,6 +10,7 @@ return (static function (string $extensionKey, string $tableName) { 'ctrl' => [ 'label' => 'title', 'iconfile' => \WerkraumMedia\ThueCat\Extension::getIconPath() . $tableName . '.svg', + 'type' => 'type', 'default_sortby' => 'title', 'tstamp' => 'tstamp', 'crdate' => 'crdate', @@ -40,6 +41,10 @@ return (static function (string $extensionKey, string $tableName) { $languagePath . '.type.static', 'static', ], + [ + $languagePath . '.type.syncScope', + 'syncScope', + ], ], ], ], @@ -51,6 +56,7 @@ return (static function (string $extensionKey, string $tableName) { 'ds' => [ 'default' => $flexFormConfigurationPath . 'ImportConfiguration/Static.xml', 'static' => $flexFormConfigurationPath . 'ImportConfiguration/Static.xml', + 'syncScope' => $flexFormConfigurationPath . 'ImportConfiguration/SyncScope.xml', ], ], ], diff --git a/README.md b/README.md index 2bc98ee..e7f5504 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # ThüCAT integration into TYPO3 CMS - ThüCAT is ¨Thüringer Content Architektur Tourismus¨. This is an extension for TYPO3 CMS (https://typo3.org/) to integrate ThüCAT. The existing API is integrated and allows importing data into the system. @@ -9,8 +8,12 @@ The existing API is integrated and allows importing data into the system. The extension already allows: -* Create static configuration to import specified resources, - e.g. defined organisation or towns. +* Create configuration to import: + + * specified resources via static configuration, + e.g. defined organisation or towns. + + * sync scope, a syncScopeId to always update delivered resources. * Support multiple languages @@ -47,3 +50,10 @@ The extension already allows: * Content element to display town, tourist information and organisation. * Extending import to include further properties + +## Installation + +Please configure API Key via Extension Configuration. + +Configuration records need to be created, e.g. by visiting the ThüCAT module. +Those can then be imported via the same module. diff --git a/Resources/Private/Language/locallang_conf.xlf b/Resources/Private/Language/locallang_conf.xlf new file mode 100644 index 0000000..e5ac0f1 --- /dev/null +++ b/Resources/Private/Language/locallang_conf.xlf @@ -0,0 +1,11 @@ + + + +
+ + + API-Key + + + + diff --git a/Resources/Private/Language/locallang_flexform.xlf b/Resources/Private/Language/locallang_flexform.xlf index a3d39a7..84bc748 100644 --- a/Resources/Private/Language/locallang_flexform.xlf +++ b/Resources/Private/Language/locallang_flexform.xlf @@ -3,6 +3,7 @@
+ Static import configuration @@ -16,6 +17,17 @@ URL + + + Sync Scope import configuration + + + Storage Page UID + + + syncScopeId + + Tourist Attraction diff --git a/Resources/Private/Language/locallang_tca.xlf b/Resources/Private/Language/locallang_tca.xlf index b1a7bc0..b5d0c9d 100644 --- a/Resources/Private/Language/locallang_tca.xlf +++ b/Resources/Private/Language/locallang_tca.xlf @@ -130,6 +130,9 @@ Static list of URLs + + Synchronization area + Configuration diff --git a/Tests/Functional/Fixtures/Import/Guzzle/cdb.thuecat.org/api/ext-sync/get-updated-nodes/GET_51cb06caa2f1ca6e989e10b0ee7d9b0f.txt b/Tests/Functional/Fixtures/Import/Guzzle/cdb.thuecat.org/api/ext-sync/get-updated-nodes/GET_51cb06caa2f1ca6e989e10b0ee7d9b0f.txt new file mode 100644 index 0000000..2cba660 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/Guzzle/cdb.thuecat.org/api/ext-sync/get-updated-nodes/GET_51cb06caa2f1ca6e989e10b0ee7d9b0f.txt @@ -0,0 +1,19 @@ +HTTP/1.1 200 OK +Date: Mon, 31 May 2021 07:45:26 GMT +Content-Type: application/json; charset=utf-8 +Content-Length: 34 +Connection: keep-alive +access-control-allow-origin: https://cdb.thuecat.org +content-security-policy: default-src 'self'; script-src 'self' 'sha256-xfTbtWk8kVI65iLJs8LB3lWf2g0g10DS71pDdoutFHc='; style-src 'self' 'unsafe-inline' https://stackpath.bootstrapcdn.com; img-src 'self' data: blob: * +feature-policy: microphone 'none'; camera 'none'; payment 'none' +referrer-policy: same-origin +x-content-type-options: nosniff +x-xss-protection: 1; mode=block +x-frame-options: deny +access-control-allow-credentials: true +strict-transport-security: max-age=15724800; includeSubDomains +access-control-allow-headers: Authorization, Content-Type +access-control-allow-methods: HEAD, GET, POST, DELETE, OPTIONS +set-cookie: ahSession=3d594be0a8f63b6e5aa0683d86c33f0014462fff;path=/;expires=Thu, 01 Jul 2021 07:45:26 GMT;httpOnly=true; + +{"data":{"createdOrUpdated":["835224016581-dara","165868194223-zmqf","215230952334-yyno"],"removed":["319489049949-yzpe","440865870518-kcka","057564926026-ambc","502105041571-gtmz","956950809461-mkyx","505212346932-dgdj","304166137220-qegp","052993102595-yytg","008779699609-ettg","992865433390-jqcw","678174286034-dpza","473249269683-mxjj","r_20704386-oapoi","121412224073-roqx","067447662224-fhpb","103385129122-pypq","764328419582-bdhj","303605630412-cygb","891743863902-bkeb"]}} diff --git a/Tests/Functional/Fixtures/Import/Guzzle/cdb.thuecat.org/api/ext-sync/get-updated-nodes/GET_c4796659430e72ef994a68ca29ac5e8c.txt b/Tests/Functional/Fixtures/Import/Guzzle/cdb.thuecat.org/api/ext-sync/get-updated-nodes/GET_c4796659430e72ef994a68ca29ac5e8c.txt new file mode 100644 index 0000000..2cba660 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/Guzzle/cdb.thuecat.org/api/ext-sync/get-updated-nodes/GET_c4796659430e72ef994a68ca29ac5e8c.txt @@ -0,0 +1,19 @@ +HTTP/1.1 200 OK +Date: Mon, 31 May 2021 07:45:26 GMT +Content-Type: application/json; charset=utf-8 +Content-Length: 34 +Connection: keep-alive +access-control-allow-origin: https://cdb.thuecat.org +content-security-policy: default-src 'self'; script-src 'self' 'sha256-xfTbtWk8kVI65iLJs8LB3lWf2g0g10DS71pDdoutFHc='; style-src 'self' 'unsafe-inline' https://stackpath.bootstrapcdn.com; img-src 'self' data: blob: * +feature-policy: microphone 'none'; camera 'none'; payment 'none' +referrer-policy: same-origin +x-content-type-options: nosniff +x-xss-protection: 1; mode=block +x-frame-options: deny +access-control-allow-credentials: true +strict-transport-security: max-age=15724800; includeSubDomains +access-control-allow-headers: Authorization, Content-Type +access-control-allow-methods: HEAD, GET, POST, DELETE, OPTIONS +set-cookie: ahSession=3d594be0a8f63b6e5aa0683d86c33f0014462fff;path=/;expires=Thu, 01 Jul 2021 07:45:26 GMT;httpOnly=true; + +{"data":{"createdOrUpdated":["835224016581-dara","165868194223-zmqf","215230952334-yyno"],"removed":["319489049949-yzpe","440865870518-kcka","057564926026-ambc","502105041571-gtmz","956950809461-mkyx","505212346932-dgdj","304166137220-qegp","052993102595-yytg","008779699609-ettg","992865433390-jqcw","678174286034-dpza","473249269683-mxjj","r_20704386-oapoi","121412224073-roqx","067447662224-fhpb","103385129122-pypq","764328419582-bdhj","303605630412-cygb","891743863902-bkeb"]}} diff --git a/Tests/Functional/Fixtures/Import/ImportsSyncScope.xml b/Tests/Functional/Fixtures/Import/ImportsSyncScope.xml new file mode 100644 index 0000000..25816f4 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/ImportsSyncScope.xml @@ -0,0 +1,91 @@ + + + + 1 + 0 + 1613400587 + 1613400558 + 1 + 4 + Rootpage + 1 + + + 10 + 1 + 1613400587 + 1613400558 + 1 + 254 + Storage folder + + + + 1 + 0 + English + en-us-gb + en + + + + 2 + 0 + French + fr + fr + + + + 1 + 0 + 1613400587 + 1613400558 + 1 + 0 + Sync Scope ID + syncScope + + + + + + + 10 + + + dd4615dc-58a6-4648-a7ce-4950293a06db + + + + + ]]> + + + + 1 + 10 + 1613401129 + 1613401129 + 1 + 0 + https://thuecat.org/resources/043064193523-jcyt + 1 + 0 + Erfurt + + + + 1 + 10 + 1613400969 + 1613400969 + 1 + 0 + https://thuecat.org/resources/018132452787-ngbe + Erfurt Tourismus und Marketing GmbH + Die Erfurt Tourismus & Marketing GmbH (ETMG) wurde 1997 als offizielle Organisation zur Tourismusförderung in der Landeshauptstadt Erfurt gegründet und nahm am 01.0 1.1998 die Geschäftstätigkeit auf. + 0 + 0 + + diff --git a/Tests/Functional/Fixtures/Import/ImportsSyncScopeResult.csv b/Tests/Functional/Fixtures/Import/ImportsSyncScopeResult.csv new file mode 100644 index 0000000..aae1312 --- /dev/null +++ b/Tests/Functional/Fixtures/Import/ImportsSyncScopeResult.csv @@ -0,0 +1,10 @@ +tx_thuecat_tourist_attraction +,"uid","pid","sys_language_uid","l18n_parent","l10n_source","l10n_state","remote_id","title","managed_by","town","address","offers" +,1,10,0,0,0,\NULL,"https://thuecat.org/resources/835224016581-dara","Dom St. Marien",1,1,"{""street"":""Domstufen 1"",""zip"":""99084"",""city"":""Erfurt"",""email"":""dominformation@domberg-erfurt.de"",""phone"":""+49 361 6461265"",""fax"":"""",""geo"":{""latitude"":50.975955358589545,""longitude"":11.023667024961856}}","[]" +,2,10,1,1,1,\NULL,"https://thuecat.org/resources/835224016581-dara","Cathedral of St. Mary",1,1,"{""street"":""Domstufen 1"",""zip"":""99084"",""city"":""Erfurt"",""email"":""dominformation@domberg-erfurt.de"",""phone"":""+49 361 6461265"",""fax"":"""",""geo"":{""latitude"":50.975955358589545,""longitude"":11.023667024961856}}","[]" +,3,10,0,0,0,\NULL,"https://thuecat.org/resources/165868194223-zmqf","Alte Synagoge",1,1,"{""street"":""Waagegasse 8"",""zip"":""99084"",""city"":""Erfurt"",""email"":""altesynagoge@erfurt.de"",""phone"":""+49 361 6551520"",""fax"":""+49 361 6551669"",""geo"":{""latitude"":50.978765,""longitude"":11.029133}}","[{""title"":""F\u00fchrungen"",""description"":""Immer samstags, um 11:15 Uhr findet eine \u00f6ffentliche F\u00fchrung durch das Museum statt. Dauer etwa 90 Minuten"",""prices"":[{""title"":""Erwachsene"",""description"":"""",""price"":8,""currency"":""EUR"",""rule"":""PerPerson""},{""title"":""Erm\u00e4\u00dfigt"",""description"":""als erm\u00e4\u00dfigt gelten schulpflichtige Kinder, Auszubildende, Studierende, Rentner\/-innen, Menschen mit Behinderungen, Inhaber Sozialausweis der Landeshauptstadt Erfurt"",""price"":5,""currency"":""EUR"",""rule"":""PerPerson""}]},{""title"":""Eintritt"",""description"":""Schulklassen und Kitagruppen im Rahmen des Unterrichts: Eintritt frei\nAn jedem ersten Dienstag im Monat: Eintritt frei"",""prices"":[{""title"":""Erm\u00e4\u00dfigt"",""description"":""als erm\u00e4\u00dfigt gelten schulpflichtige Kinder, Auszubildende, Studierende, Rentner\/-innen, Menschen mit Behinderungen, Inhaber Sozialausweis der Landeshauptstadt Erfurt"",""price"":5,""currency"":""EUR"",""rule"":""PerPerson""},{""title"":""Familienkarte"",""description"":"""",""price"":17,""currency"":""EUR"",""rule"":""PerGroup""},{""title"":""ErfurtCard"",""description"":"""",""price"":14.9,""currency"":""EUR"",""rule"":""PerPackage""},{""title"":""Erwachsene"",""description"":"""",""price"":8,""currency"":""EUR"",""rule"":""PerPerson""}]}]" +,4,10,1,3,3,\NULL,"https://thuecat.org/resources/165868194223-zmqf","Old Synagogue",1,1,"{""street"":""Waagegasse 8"",""zip"":""99084"",""city"":""Erfurt"",""email"":""altesynagoge@erfurt.de"",""phone"":""+49 361 6551520"",""fax"":""+49 361 6551669"",""geo"":{""latitude"":50.978765,""longitude"":11.029133}}","[{""title"":""F\u00fchrungen"",""description"":""Immer samstags, um 11:15 Uhr findet eine \u00f6ffentliche F\u00fchrung durch das Museum statt. Dauer etwa 90 Minuten"",""prices"":[{""title"":""Erwachsene"",""description"":"""",""price"":8,""currency"":""EUR"",""rule"":""PerPerson""},{""title"":""Erm\u00e4\u00dfigt"",""description"":""als erm\u00e4\u00dfigt gelten schulpflichtige Kinder, Auszubildende, Studierende, Rentner\/-innen, Menschen mit Behinderungen, Inhaber Sozialausweis der Landeshauptstadt Erfurt"",""price"":5,""currency"":""EUR"",""rule"":""PerPerson""}]},{""title"":""Eintritt"",""description"":""Schulklassen und Kitagruppen im Rahmen des Unterrichts: Eintritt frei\nAn jedem ersten Dienstag im Monat: Eintritt frei"",""prices"":[{""title"":""Erm\u00e4\u00dfigt"",""description"":""als erm\u00e4\u00dfigt gelten schulpflichtige Kinder, Auszubildende, Studierende, Rentner\/-innen, Menschen mit Behinderungen, Inhaber Sozialausweis der Landeshauptstadt Erfurt"",""price"":5,""currency"":""EUR"",""rule"":""PerPerson""},{""title"":""Familienkarte"",""description"":"""",""price"":17,""currency"":""EUR"",""rule"":""PerGroup""},{""title"":""ErfurtCard"",""description"":"""",""price"":14.9,""currency"":""EUR"",""rule"":""PerPackage""},{""title"":""Erwachsene"",""description"":"""",""price"":8,""currency"":""EUR"",""rule"":""PerPerson""}]}]" +,5,10,2,3,3,\NULL,"https://thuecat.org/resources/165868194223-zmqf","La vieille synagogue",1,1,"{""street"":""Waagegasse 8"",""zip"":""99084"",""city"":""Erfurt"",""email"":""altesynagoge@erfurt.de"",""phone"":""+49 361 6551520"",""fax"":""+49 361 6551669"",""geo"":{""latitude"":50.978765,""longitude"":11.029133}}","[{""title"":""F\u00fchrungen"",""description"":""Immer samstags, um 11:15 Uhr findet eine \u00f6ffentliche F\u00fchrung durch das Museum statt. Dauer etwa 90 Minuten"",""prices"":[{""title"":""Erwachsene"",""description"":"""",""price"":8,""currency"":""EUR"",""rule"":""PerPerson""},{""title"":""Erm\u00e4\u00dfigt"",""description"":""als erm\u00e4\u00dfigt gelten schulpflichtige Kinder, Auszubildende, Studierende, Rentner\/-innen, Menschen mit Behinderungen, Inhaber Sozialausweis der Landeshauptstadt Erfurt"",""price"":5,""currency"":""EUR"",""rule"":""PerPerson""}]},{""title"":""Eintritt"",""description"":""Schulklassen und Kitagruppen im Rahmen des Unterrichts: Eintritt frei\nAn jedem ersten Dienstag im Monat: Eintritt frei"",""prices"":[{""title"":""Erm\u00e4\u00dfigt"",""description"":""als erm\u00e4\u00dfigt gelten schulpflichtige Kinder, Auszubildende, Studierende, Rentner\/-innen, Menschen mit Behinderungen, Inhaber Sozialausweis der Landeshauptstadt Erfurt"",""price"":5,""currency"":""EUR"",""rule"":""PerPerson""},{""title"":""Familienkarte"",""description"":"""",""price"":17,""currency"":""EUR"",""rule"":""PerGroup""},{""title"":""ErfurtCard"",""description"":"""",""price"":14.9,""currency"":""EUR"",""rule"":""PerPackage""},{""title"":""Erwachsene"",""description"":"""",""price"":8,""currency"":""EUR"",""rule"":""PerPerson""}]}]" +,6,10,0,0,0,\NULL,"https://thuecat.org/resources/215230952334-yyno","Krämerbrücke",1,1,"{""street"":""Benediktsplatz 1"",""zip"":""99084"",""city"":""Erfurt"",""email"":""service@erfurt-tourismus.de"",""phone"":""+49 361 66 400"",""fax"":"""",""geo"":{""latitude"":50.978772,""longitude"":11.031622}}","[]" +,7,10,1,6,6,\NULL,"https://thuecat.org/resources/215230952334-yyno","Merchants' Bridge",1,1,"{""street"":""Benediktsplatz 1"",""zip"":""99084"",""city"":""Erfurt"",""email"":""service@erfurt-tourismus.de"",""phone"":""+49 361 66 400"",""fax"":"""",""geo"":{""latitude"":50.978772,""longitude"":11.031622}}","[]" +,8,10,2,6,6,\NULL,"https://thuecat.org/resources/215230952334-yyno","Pont de l'épicier",1,1,"{""street"":""Benediktsplatz 1"",""zip"":""99084"",""city"":""Erfurt"",""email"":""service@erfurt-tourismus.de"",""phone"":""+49 361 66 400"",""fax"":"""",""geo"":{""latitude"":50.978772,""longitude"":11.031622}}","[]" diff --git a/Tests/Functional/ImportTest.php b/Tests/Functional/ImportTest.php index a7842a2..2ea1fc0 100644 --- a/Tests/Functional/ImportTest.php +++ b/Tests/Functional/ImportTest.php @@ -91,6 +91,14 @@ class ImportTest extends TestCase 'typo3conf/ext/thuecat/Tests/Functional/Fixtures/Import/Sites/' => 'typo3conf/sites', ]; + protected $configurationToUseInTestInstance = [ + 'EXTENSIONS' => [ + 'thuecat' => [ + 'apiKey' => null, + ], + ], + ]; + protected function setUp(): void { parent::setUp(); @@ -212,6 +220,24 @@ class ImportTest extends TestCase $this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsTouristAttractionsWithRelationsResult.csv'); } + /** + * @test + */ + public function importsBasedOnSyncScope(): void + { + $this->importDataSet(__DIR__ . '/Fixtures/Import/ImportsSyncScope.xml'); + + $serverRequest = $this->getServerRequest(); + + $extbaseBootstrap = $this->getContainer()->get(Bootstrap::class); + $extbaseBootstrap->handleBackendRequest($serverRequest->reveal()); + + $touristAttractions = $this->getAllRecords('tx_thuecat_tourist_attraction'); + self::assertCount(8, $touristAttractions); + + $this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsSyncScopeResult.csv'); + } + /** * @return ObjectProphecy */ diff --git a/Tests/Unit/Domain/Import/Importer/FetchDataTest.php b/Tests/Unit/Domain/Import/Importer/FetchDataTest.php index a9ffd82..1276017 100644 --- a/Tests/Unit/Domain/Import/Importer/FetchDataTest.php +++ b/Tests/Unit/Domain/Import/Importer/FetchDataTest.php @@ -31,6 +31,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use WerkraumMedia\ThueCat\Domain\Import\Importer\FetchData; +use WerkraumMedia\ThueCat\Domain\Import\Importer\FetchData\InvalidResponseException; /** * @covers WerkraumMedia\ThueCat\Domain\Import\Importer\FetchData @@ -75,6 +76,7 @@ class FetchDataTest extends TestCase $httpClient->sendRequest($request->reveal()) ->willReturn($response->reveal()); + $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn('{"@graph":[{"@id":"https://example.com/resources/018132452787-ngbe"}]}'); $subject = new FetchData( @@ -111,6 +113,7 @@ class FetchDataTest extends TestCase $httpClient->sendRequest($request->reveal()) ->willReturn($response->reveal()); + $response->getStatusCode()->willReturn(200); $response->getBody()->willReturn(''); $subject = new FetchData( @@ -155,4 +158,74 @@ class FetchDataTest extends TestCase ], ], $result); } + + /** + * @test + */ + public function throwsExceptionOn404(): void + { + $requestFactory = $this->prophesize(RequestFactoryInterface::class); + $httpClient = $this->prophesize(ClientInterface::class); + $cache = $this->prophesize(FrontendInterface::class); + + $request = $this->prophesize(RequestInterface::class); + $response = $this->prophesize(ResponseInterface::class); + + $request->getUri()->willReturn('https://example.com/resources/018132452787-ngbe'); + + $requestFactory->createRequest('GET', 'https://example.com/resources/018132452787-ngbe') + ->willReturn($request->reveal()); + + $httpClient->sendRequest($request->reveal()) + ->willReturn($response->reveal()); + + + $response->getStatusCode()->willReturn(404); + $response->getBody()->willReturn('{"error":"404"}'); + + $subject = new FetchData( + $requestFactory->reveal(), + $httpClient->reveal(), + $cache->reveal() + ); + + $this->expectException(InvalidResponseException::class); + $this->expectExceptionCode(1622461820); + $this->expectExceptionMessage('Not found, given resource could not be found: "https://example.com/resources/018132452787-ngbe".'); + + $subject->jsonLDFromUrl('https://example.com/resources/018132452787-ngbe'); + } + + /** + * @test + */ + public function throwsExceptionOn401(): void + { + $requestFactory = $this->prophesize(RequestFactoryInterface::class); + $httpClient = $this->prophesize(ClientInterface::class); + $cache = $this->prophesize(FrontendInterface::class); + + $request = $this->prophesize(RequestInterface::class); + $response = $this->prophesize(ResponseInterface::class); + + $requestFactory->createRequest('GET', 'https://example.com/resources/018132452787-ngbe') + ->willReturn($request->reveal()); + + $httpClient->sendRequest($request->reveal()) + ->willReturn($response->reveal()); + + $response->getStatusCode()->willReturn(401); + + $subject = new FetchData( + $requestFactory->reveal(), + $httpClient->reveal(), + $cache->reveal() + ); + + $this->expectException(InvalidResponseException::class); + $this->expectExceptionCode(1622461709); + $this->expectExceptionMessage('Unauthorized API request, ensure apiKey is properly configured.'); + + $subject->jsonLDFromUrl('https://example.com/resources/018132452787-ngbe'); + } } diff --git a/Tests/Unit/Domain/Import/RequestFactoryTest.php b/Tests/Unit/Domain/Import/RequestFactoryTest.php index e222263..9053338 100644 --- a/Tests/Unit/Domain/Import/RequestFactoryTest.php +++ b/Tests/Unit/Domain/Import/RequestFactoryTest.php @@ -24,6 +24,9 @@ namespace WerkraumMedia\ThueCat\Tests\Unit\Domain\Import; */ use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationExtensionNotConfiguredException; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use WerkraumMedia\ThueCat\Domain\Import\RequestFactory; /** @@ -31,12 +34,18 @@ use WerkraumMedia\ThueCat\Domain\Import\RequestFactory; */ class RequestFactoryTest extends TestCase { + use ProphecyTrait; + /** * @test */ public function canBeCreated(): void { - $subject = new RequestFactory(); + $extensionConfiguration = $this->prophesize(ExtensionConfiguration::class); + + $subject = new RequestFactory( + $extensionConfiguration->reveal() + ); self::assertInstanceOf(RequestFactory::class, $subject); } @@ -46,9 +55,48 @@ class RequestFactoryTest extends TestCase */ public function returnsRequestWithJsonIdFormat(): void { - $subject = new RequestFactory(); - $request = $subject->createRequest('GET', 'https://example.com/resources/333039283321-xxwg'); + $extensionConfiguration = $this->prophesize(ExtensionConfiguration::class); - self::assertSame('format=jsonld', $request->getUri()->getQuery()); + $subject = new RequestFactory( + $extensionConfiguration->reveal() + ); + + $request = $subject->createRequest('GET', 'https://example.com/api/ext-sync/get-updated-nodes?syncScopeId=dd3738dc-58a6-4748-a6ce-4950293a06db'); + + self::assertSame('syncScopeId=dd3738dc-58a6-4748-a6ce-4950293a06db&format=jsonld', $request->getUri()->getQuery()); + } + + /** + * @test + */ + public function returnsRequestWithApiKeyWhenConfigured(): void + { + $extensionConfiguration = $this->prophesize(ExtensionConfiguration::class); + $extensionConfiguration->get('thuecat', 'apiKey')->willReturn('some-api-key'); + + $subject = new RequestFactory( + $extensionConfiguration->reveal() + ); + + $request = $subject->createRequest('GET', 'https://example.com/api/ext-sync/get-updated-nodes?syncScopeId=dd3738dc-58a6-4748-a6ce-4950293a06db'); + + self::assertSame('syncScopeId=dd3738dc-58a6-4748-a6ce-4950293a06db&format=jsonld&api_key=some-api-key', $request->getUri()->getQuery()); + } + + /** + * @test + */ + public function returnsRequestWithoutApiKeyWhenUnkown(): void + { + $extensionConfiguration = $this->prophesize(ExtensionConfiguration::class); + $extensionConfiguration->get('thuecat', 'apiKey')->willThrow(new ExtensionConfigurationExtensionNotConfiguredException()); + + $subject = new RequestFactory( + $extensionConfiguration->reveal() + ); + + $request = $subject->createRequest('GET', 'https://example.com/api/ext-sync/get-updated-nodes?syncScopeId=dd3738dc-58a6-4748-a6ce-4950293a06db'); + + self::assertSame('syncScopeId=dd3738dc-58a6-4748-a6ce-4950293a06db&format=jsonld', $request->getUri()->getQuery()); } } diff --git a/Tests/Unit/Domain/Import/UrlProvider/StaticUrlProviderTest.php b/Tests/Unit/Domain/Import/UrlProvider/StaticUrlProviderTest.php index a9a593b..7d32da0 100644 --- a/Tests/Unit/Domain/Import/UrlProvider/StaticUrlProviderTest.php +++ b/Tests/Unit/Domain/Import/UrlProvider/StaticUrlProviderTest.php @@ -40,10 +40,7 @@ class StaticUrlProviderTest extends TestCase */ public function canBeCreated(): void { - $configuration = $this->prophesize(ImportConfiguration::class); - $configuration->getUrls()->willReturn([]); - - $subject = new StaticUrlProvider($configuration->reveal()); + $subject = new StaticUrlProvider(); self::assertInstanceOf(StaticUrlProvider::class, $subject); } @@ -56,7 +53,7 @@ class StaticUrlProviderTest extends TestCase $configuration->getUrls()->willReturn([]); $configuration->getType()->willReturn('static'); - $subject = new StaticUrlProvider($configuration->reveal()); + $subject = new StaticUrlProvider(); $result = $subject->canProvideForConfiguration($configuration->reveal()); self::assertTrue($result); @@ -70,7 +67,7 @@ class StaticUrlProviderTest extends TestCase $configuration = $this->prophesize(ImportConfiguration::class); $configuration->getUrls()->willReturn(['https://example.com']); - $subject = new StaticUrlProvider($configuration->reveal()); + $subject = new StaticUrlProvider(); $result = $subject->createWithConfiguration($configuration->reveal()); self::assertInstanceOf(StaticUrlProvider::class, $subject); @@ -84,7 +81,7 @@ class StaticUrlProviderTest extends TestCase $configuration = $this->prophesize(ImportConfiguration::class); $configuration->getUrls()->willReturn(['https://example.com']); - $subject = new StaticUrlProvider($configuration->reveal()); + $subject = new StaticUrlProvider(); $concreteProvider = $subject->createWithConfiguration($configuration->reveal()); $result = $concreteProvider->getUrls(); diff --git a/Tests/Unit/Domain/Model/Backend/ImportConfigurationTest.php b/Tests/Unit/Domain/Model/Backend/ImportConfigurationTest.php new file mode 100644 index 0000000..9c7bfc4 --- /dev/null +++ b/Tests/Unit/Domain/Model/Backend/ImportConfigurationTest.php @@ -0,0 +1,333 @@ + + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase as TestCase; +use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportConfiguration; + +/** + * @covers \WerkraumMedia\ThueCat\Domain\Model\Backend\ImportConfiguration + */ +class ImportConfigurationTest extends TestCase +{ + /** + * @test + */ + public function canBeCreated(): void + { + $subject = new ImportConfiguration(); + + self::assertInstanceOf(ImportConfiguration::class, $subject); + } + + /** + * @test + */ + public function returnsTitle(): void + { + $subject = new ImportConfiguration(); + $subject->_setProperty('title', 'Example Title'); + + self::assertSame('Example Title', $subject->getTitle()); + } + + /** + * @test + */ + public function returnsType(): void + { + $subject = new ImportConfiguration(); + $subject->_setProperty('type', 'static'); + + self::assertSame('static', $subject->getType()); + } + + /** + * @test + */ + public function returnsTableName(): void + { + $subject = new ImportConfiguration(); + + self::assertSame('tx_thuecat_import_configuration', $subject->getTableName()); + } + + /** + * @test + */ + public function returnsLastChanged(): void + { + $lastChanged = new \DateTimeImmutable(); + + $subject = new ImportConfiguration(); + + $subject->_setProperty('tstamp', $lastChanged); + + self::assertSame($lastChanged, $subject->getLastChanged()); + } + + /** + * @test + */ + public function returnsStoragePidWhenSet(): void + { + $flexForm = implode(PHP_EOL, [ + '', + '', + '', + '', + '', + '', + '20', + '', + '', + '', + '', + '', + ]); + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame(20, $subject->getStoragePid()); + } + + /** + * @test + */ + public function returnsZeroAsStoragePidWhenNoConfigurationExists(): void + { + $flexForm = ''; + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame(0, $subject->getStoragePid()); + } + + /** + * @test + */ + public function returnsZeroAsStoragePidWhenNegativePidIsConfigured(): void + { + $flexForm = implode(PHP_EOL, [ + '', + '', + '', + '', + '', + '', + '-1', + '', + '', + '', + '', + '', + ]); + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame(0, $subject->getStoragePid()); + } + + /** + * @test + */ + public function returnsZeroAsStoragePidWhenNoneNumericPidIsConfigured(): void + { + $flexForm = implode(PHP_EOL, [ + '', + '', + '', + '', + '', + '', + 'abc', + '', + '', + '', + '', + '', + ]); + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame(0, $subject->getStoragePid()); + } + + /** + * @test + */ + public function returnsUrlsWhenSet(): void + { + $flexForm = implode(PHP_EOL, [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + 'https://thuecat.org/resources/942302009360-jopp', + '', + '', + '', + '0', + '', + '', + '', + '', + '', + '', + '', + ]); + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame([ + 'https://thuecat.org/resources/942302009360-jopp', + ], $subject->getUrls()); + } + + /** + * @test + */ + public function returnsEmptyArrayAsUrlsWhenNoConfigurationExists(): void + { + $flexForm = ''; + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame([], $subject->getUrls()); + } + + /** + * @test + */ + public function returnsEmptyArrayAsUrlsWhenNoUrlsAreConfigured(): void + { + $flexForm = implode(PHP_EOL, [ + '', + '', + '', + '', + '', + '', + '10', + '', + '', + '', + '', + '', + ]); + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame([], $subject->getUrls()); + } + + /** + * @test + */ + public function returnsSyncScopeIdWhenSet(): void + { + $flexForm = implode(PHP_EOL, [ + '', + '', + '', + '', + '', + '', + 'dd4639dc-58a7-4648-a6ce-4950293a06db', + '', + '', + '', + '', + '', + ]); + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame('dd4639dc-58a7-4648-a6ce-4950293a06db', $subject->getSyncScopeId()); + } + + /** + * @test + */ + public function returnsEmptyStringAsSyncScopeIdWhenNoConfigurationExists(): void + { + $flexForm = ''; + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame('', $subject->getSyncScopeId()); + } + + /** + * @test + */ + public function returnsEmptyStringAsSyncScopeIdWhenNoSyncScopeIdAreConfigured(): void + { + $flexForm = implode(PHP_EOL, [ + '', + '', + '', + '', + '', + '', + '10', + '', + '', + '', + '', + '', + ]); + + $subject = new ImportConfiguration(); + + $subject->_setProperty('configuration', $flexForm); + + self::assertSame('', $subject->getSyncScopeId()); + } +} diff --git a/ext_conf_template.txt b/ext_conf_template.txt new file mode 100644 index 0000000..adacf86 --- /dev/null +++ b/ext_conf_template.txt @@ -0,0 +1,2 @@ +# cat=API; type=string; label=LLL:EXT:thuecat/Resources/Private/Language/locallang_conf.xlf:apiKey +apiKey =