Support sync scope

Add new configuration to support sync scope.
This one requires the provided sync scope id and will always update all
entries.

Relates: #23
pull/26/head
Daniel Siepmann 2 years ago
parent 22932545d3
commit 197a3e4696
  1. 55
      Classes/Domain/Import/Importer/FetchData.php
  2. 26
      Classes/Domain/Import/Importer/FetchData/InvalidResponseException.php
  3. 28
      Classes/Domain/Import/RequestFactory.php
  4. 14
      Classes/Domain/Import/UrlProvider/StaticUrlProvider.php
  5. 74
      Classes/Domain/Import/UrlProvider/SyncScopeUrlProvider.php
  6. 34
      Classes/Domain/Model/Backend/ImportConfiguration.php
  7. 35
      Configuration/FlexForm/ImportConfiguration/SyncScope.xml
  8. 6
      Configuration/TCA/tx_thuecat_import_configuration.php
  9. 16
      README.md
  10. 11
      Resources/Private/Language/locallang_conf.xlf
  11. 12
      Resources/Private/Language/locallang_flexform.xlf
  12. 3
      Resources/Private/Language/locallang_tca.xlf
  13. 19
      Tests/Functional/Fixtures/Import/Guzzle/cdb.thuecat.org/api/ext-sync/get-updated-nodes/GET_51cb06caa2f1ca6e989e10b0ee7d9b0f.txt
  14. 19
      Tests/Functional/Fixtures/Import/Guzzle/cdb.thuecat.org/api/ext-sync/get-updated-nodes/GET_c4796659430e72ef994a68ca29ac5e8c.txt
  15. 91
      Tests/Functional/Fixtures/Import/ImportsSyncScope.xml
  16. 10
      Tests/Functional/Fixtures/Import/ImportsSyncScopeResult.csv
  17. 26
      Tests/Functional/ImportTest.php
  18. 73
      Tests/Unit/Domain/Import/Importer/FetchDataTest.php
  19. 56
      Tests/Unit/Domain/Import/RequestFactoryTest.php
  20. 11
      Tests/Unit/Domain/Import/UrlProvider/StaticUrlProviderTest.php
  21. 333
      Tests/Unit/Domain/Model/Backend/ImportConfigurationTest.php
  22. 2
      ext_conf_template.txt

@ -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
);
}
}
}

@ -0,0 +1,26 @@
<?php
namespace WerkraumMedia\ThueCat\Domain\Import\Importer\FetchData;
/*
* Copyright (C) 2021 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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
{
}

@ -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);
}

@ -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

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace WerkraumMedia\ThueCat\Domain\Import\UrlProvider;
/*
* Copyright (C) 2021 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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;
}
}

@ -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);
}
}

@ -0,0 +1,35 @@
<T3DataStructure>
<meta>
<langDisable>1</langDisable>
</meta>
<sheets>
<sDEF>
<ROOT>
<TCEforms>
<sheetTitle>LLL:EXT:thuecat/Resources/Private/Language/locallang_flexform.xlf:importConfiguration.syncScope.sheetTitle</sheetTitle>
</TCEforms>
<type>array</type>
<el>
<storagePid>
<TCEforms>
<label>LLL:EXT:thuecat/Resources/Private/Language/locallang_flexform.xlf:importConfiguration.syncScope.storagePid</label>
<config>
<type>input</type>
<eval>int,required</eval>
</config>
</TCEforms>
</storagePid>
<syncScopeId>
<TCEforms>
<label>LLL:EXT:thuecat/Resources/Private/Language/locallang_flexform.xlf:importConfiguration.syncScope.syncScopeId</label>
<config>
<type>input</type>
<eval>trim,required</eval>
</config>
</TCEforms>
</syncScopeId>
</el>
</ROOT>
</sDEF>
</sheets>
</T3DataStructure>

@ -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',
],
],
],

@ -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.

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="messages" date="2021-02-01T09:24:10Z" product-name="ThueCat TCA Labels">
<header/>
<body>
<trans-unit id="apiKey" xml:space="preserve">
<source>API-Key</source>
</trans-unit>
</body>
</file>
</xliff>

@ -3,6 +3,7 @@
<file source-language="en" datatype="plaintext" original="messages" date="2021-02-01T09:24:10Z" product-name="ThueCat FlexForms Labels">
<header/>
<body>
<!-- Static Import Configuration -->
<trans-unit id="importConfiguration.static.sheetTitle" xml:space="preserve">
<source>Static import configuration</source>
</trans-unit>
@ -16,6 +17,17 @@
<source>URL</source>
</trans-unit>
<!-- Sync Scope Import Configuration -->
<trans-unit id="importConfiguration.syncScope.sheetTitle" xml:space="preserve">
<source>Sync Scope import configuration</source>
</trans-unit>
<trans-unit id="importConfiguration.syncScope.storagePid" xml:space="preserve">
<source>Storage Page UID</source>
</trans-unit>
<trans-unit id="importConfiguration.syncScope.syncScopeId" xml:space="preserve">
<source>syncScopeId</source>
</trans-unit>
<trans-unit id="pages.tourist_attraction.sheetTitle" xml:space="preserve">
<source>Tourist Attraction</source>
</trans-unit>

@ -130,6 +130,9 @@
<trans-unit id="tx_thuecat_import_configuration.type.static" xml:space="preserve">
<source>Static list of URLs</source>
</trans-unit>
<trans-unit id="tx_thuecat_import_configuration.type.syncScope" xml:space="preserve">
<source>Synchronization area</source>
</trans-unit>
<trans-unit id="tx_thuecat_import_configuration.configuration" xml:space="preserve">
<source>Configuration</source>
</trans-unit>

@ -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"]}}

@ -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"]}}

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<dataset>
<pages>
<uid>1</uid>
<pid>0</pid>
<tstamp>1613400587</tstamp>
<crdate>1613400558</crdate>
<cruser_id>1</cruser_id>
<doktype>4</doktype>
<title>Rootpage</title>
<is_siteroot>1</is_siteroot>
</pages>
<pages>
<uid>10</uid>
<pid>1</pid>
<tstamp>1613400587</tstamp>
<crdate>1613400558</crdate>
<cruser_id>1</cruser_id>
<doktype>254</doktype>
<title>Storage folder</title>
</pages>
<sys_language>
<uid>1</uid>
<pid>0</pid>
<title>English</title>
<flag>en-us-gb</flag>
<language_isocode>en</language_isocode>
</sys_language>
<sys_language>
<uid>2</uid>
<pid>0</pid>
<title>French</title>
<flag>fr</flag>
<language_isocode>fr</language_isocode>
</sys_language>
<tx_thuecat_import_configuration>
<uid>1</uid>
<pid>0</pid>
<tstamp>1613400587</tstamp>
<crdate>1613400558</crdate>
<cruser_id>1</cruser_id>
<disable>0</disable>
<title>Sync Scope ID</title>
<type>syncScope</type>
<configuration><![CDATA[<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<T3FlexForms>
<data>
<sheet index="sDEF">
<language index="lDEF">
<field index="storagePid">
<value index="vDEF">10</value>
</field>
<field index="syncScopeId">
<value index="vDEF">dd4615dc-58a6-4648-a7ce-4950293a06db</value>
</field>
</language>
</sheet>
</data>
</T3FlexForms>]]></configuration>
</tx_thuecat_import_configuration>
<tx_thuecat_town>
<uid>1</uid>
<pid>10</pid>
<tstamp>1613401129</tstamp>
<crdate>1613401129</crdate>
<cruser_id>1</cruser_id>
<disable>0</disable>
<remote_id>https://thuecat.org/resources/043064193523-jcyt</remote_id>
<managed_by>1</managed_by>
<tourist_information>0</tourist_information>
<title>Erfurt</title>
</tx_thuecat_town>
<tx_thuecat_organisation>
<uid>1</uid>
<pid>10</pid>
<tstamp>1613400969</tstamp>
<crdate>1613400969</crdate>
<cruser_id>1</cruser_id>
<disable>0</disable>
<remote_id>https://thuecat.org/resources/018132452787-ngbe</remote_id>
<title>Erfurt Tourismus und Marketing GmbH</title>
<description>Die Erfurt Tourismus &amp; 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.</description>
<manages_towns>0</manages_towns>
<manages_tourist_information>0</manages_tourist_information>
</tx_thuecat_organisation>
</dataset>

@ -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}}","[]"
Can't render this file because it has a wrong number of fields in line 2.

@ -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<ServerRequestInterface>
*/

@ -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');
}
}

@ -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);
$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('format=jsonld', $request->getUri()->getQuery());
self::assertSame('syncScopeId=dd3738dc-58a6-4748-a6ce-4950293a06db&format=jsonld', $request->getUri()->getQuery());
}
}

@ -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();

@ -0,0 +1,333 @@
<?php
declare(strict_types=1);
namespace WerkraumMedia\ThueCat\Tests\Unit\Domain\Model\Backend;
/*
* Copyright (C) 2021 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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, [
'<?xml version="1.0" encoding="utf-8" standalone="yes" ?>',
'<T3FlexForms>',
'<data>',
'<sheet index="sDEF">',
'<language index="lDEF">',
'<field index="storagePid">',
'<value index="vDEF">20</value>',
'</field>',
'</language>',
'</sheet>',
'</data>',
'</T3FlexForms>',
]);
$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, [
'<?xml version="1.0" encoding="utf-8" standalone="yes" ?>',
'<T3FlexForms>',
'<data>',
'<sheet index="sDEF">',
'<language index="lDEF">',
'<field index="storagePid">',
'<value index="vDEF">-1</value>',
'</field>',
'</language>',
'</sheet>',
'</data>',
'</T3FlexForms>',
]);
$subject = new ImportConfiguration();
$subject->_setProperty('configuration', $flexForm);
self::assertSame(0, $subject->getStoragePid());
}
/**
* @test
*/
public function returnsZeroAsStoragePidWhenNoneNumericPidIsConfigured(): void
{
$flexForm = implode(PHP_EOL, [
'<?xml version="1.0" encoding="utf-8" standalone="yes" ?>',
'<T3FlexForms>',
'<data>',
'<sheet index="sDEF">',
'<language index="lDEF">',
'<field index="storagePid">',
'<value index="vDEF">abc</value>',
'</field>',
'</language>',
'</sheet>',
'</data>',
'</T3FlexForms>',
]);
$subject = new ImportConfiguration();
$subject->_setProperty('configuration', $flexForm);
self::assertSame(0, $subject->getStoragePid());
}
/**
* @test
*/
public function returnsUrlsWhenSet(): void
{
$flexForm = implode(PHP_EOL, [
'<?xml version="1.0" encoding="utf-8" standalone="yes" ?>',
'<T3FlexForms>',
'<data>',
'<sheet index="sDEF">',
'<language index="lDEF">',
'<field index="urls">',
'<el index="el">',
'<field index="6098e0b6d3fff074555176">',
'<value index="url">',
'<el>',
'<field index="url">',
'<value index="vDEF">https://thuecat.org/resources/942302009360-jopp</value>',
'</field>',
'</el>',
'</value>',
'<value index="_TOGGLE">0</value>',
'</field>',
'</el>',
'</field>',
'</language>',
'</sheet>',
'</data>',
'</T3FlexForms>',
]);
$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, [
'<?xml version="1.0" encoding="utf-8" standalone="yes" ?>',
'<T3FlexForms>',
'<data>',
'<sheet index="sDEF">',
'<language index="lDEF">',
'<field index="storagePid">',
'<value index="vDEF">10</value>',
'</field>',
'</language>',
'</sheet>',
'</data>',
'</T3FlexForms>',
]);
$subject = new ImportConfiguration();
$subject->_setProperty('configuration', $flexForm);
self::assertSame([], $subject->getUrls());
}
/**
* @test
*/
public function returnsSyncScopeIdWhenSet(): void
{
$flexForm = implode(PHP_EOL, [
'<?xml version="1.0" encoding="utf-8" standalone="yes" ?>',
'<T3FlexForms>',
'<data>',
'<sheet index="sDEF">',
'<language index="lDEF">',
'<field index="syncScopeId">',
'<value index="vDEF">dd4639dc-58a7-4648-a6ce-4950293a06db</value>',
'</field>',
'</language>',
'</sheet>',
'</data>',
'</T3FlexForms>',
]);
$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, [
'<?xml version="1.0" encoding="utf-8" standalone="yes" ?>',
'<T3FlexForms>',
'<data>',
'<sheet index="sDEF">',
'<language index="lDEF">',
'<field index="storagePid">',
'<value index="vDEF">10</value>',
'</field>',
'</language>',
'</sheet>',
'</data>',
'</T3FlexForms>',
]);
$subject = new ImportConfiguration();
$subject->_setProperty('configuration', $flexForm);
self::assertSame('', $subject->getSyncScopeId());
}
}

@ -0,0 +1,2 @@
# cat=API; type=string; label=LLL:EXT:thuecat/Resources/Private/Language/locallang_conf.xlf:apiKey
apiKey =
Loading…
Cancel
Save