Browse Source

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 1 year ago
parent
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

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

26
Classes/Domain/Import/Importer/FetchData/InvalidResponseException.php

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

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

14
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

74
Classes/Domain/Import/UrlProvider/SyncScopeUrlProvider.php

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

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

35
Configuration/FlexForm/ImportConfiguration/SyncScope.xml

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

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

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

11
Resources/Private/Language/locallang_conf.xlf

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

12
Resources/Private/Language/locallang_flexform.xlf

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

3
Resources/Private/Language/locallang_tca.xlf

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

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

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

91
Tests/Functional/Fixtures/Import/ImportsSyncScope.xml

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

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

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

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

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

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

333
Tests/Unit/Domain/Model/Backend/ImportConfigurationTest.php

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

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