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
This commit is contained in:
Daniel Siepmann 2021-05-31 13:32:58 +02:00
parent 22932545d3
commit 197a3e4696
22 changed files with 927 additions and 27 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

2
ext_conf_template.txt Normal file
View file

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