diff --git a/Controller/ApiController.php b/Controller/ApiController.php index a81d885..501c382 100755 --- a/Controller/ApiController.php +++ b/Controller/ApiController.php @@ -66,8 +66,10 @@ final class ApiController extends Controller $status = NotificationLevel::ERROR; $message = 'Import failed.'; - foreach ($import['logs'] as $log) { - $this->createModel($request->header->account, $log, ExchangeLogMapper::class, 'import', $request->getOrigin()); + if (isset($import['logs'])) { + foreach ($import['logs'] as $log) { + $this->createModel($request->header->account, $log, ExchangeLogMapper::class, 'import', $request->getOrigin()); + } } if ($import['status']) { diff --git a/Models/InterfaceManager.php b/Models/InterfaceManager.php index 52d6dde..edff643 100755 --- a/Models/InterfaceManager.php +++ b/Models/InterfaceManager.php @@ -18,6 +18,7 @@ use Modules\Admin\Models\Account; use Modules\Admin\Models\NullAccount; use Modules\Media\Models\Collection; use Modules\Media\Models\NullCollection; +use phpOMS\System\File\PathException; /** * ModuleInfo class. @@ -39,6 +40,14 @@ class InterfaceManager */ protected int $id = 0; + /** + * File path. + * + * @var string + * @since 1.0.0 + */ + private string $path = ''; + /** * Title. * @@ -114,15 +123,56 @@ class InterfaceManager /** * Object constructor. * + * @param string $path Info file path + * * @since 1.0.0 */ - public function __construct() + public function __construct(string $path = '') { - $this->createdBy = new NullAccount(); - $this->createdAt = new \DateTimeImmutable('now'); + $this->path = $path; + $this->account = new NullAccount(); + $this->createdAt = new \DateTimeImmutable(); $this->source = new NullCollection(); } + /** + * Load info data from path. + * + * @return void + * + * @throws PathException this exception is thrown in case the info file path doesn't exist + * + * @since 1.0.0 + */ + public function load() : void + { + if (!\is_file($this->path)) { + throw new PathException($this->path); + } + + $contents = \file_get_contents($this->path); + + $info = \json_decode($contents === false ? '[]' : $contents, true); + + $this->title = $info['name']; + $this->version = $info['version']; + $this->website = $info['website']; + $this->hasExport = $info['export']; + $this->hasImport = $info['import']; + } + + /** + * Get info path + * + * @return string + * + * @since 1.0.0 + */ + public function getPath() : string + { + return $this->path; + } + /** * Get id * diff --git a/info.json b/info.json index c5a867a..7961161 100755 --- a/info.json +++ b/info.json @@ -18,7 +18,8 @@ "directory": "Exchange", "dependencies": { "Admin": "1.0.0", - "Media": "1.0.0" + "Media": "1.0.0", + "Job": "1.0.0" }, "providing": { "Navigation": "*" diff --git a/tests/Controller/ApiControllerTest.php b/tests/Controller/ApiControllerTest.php index 39f6245..a0d0494 100755 --- a/tests/Controller/ApiControllerTest.php +++ b/tests/Controller/ApiControllerTest.php @@ -96,14 +96,82 @@ final class ApiControllerTest extends \PHPUnit\Framework\TestCase */ public function testInterfaceInstall() : void { - $response = new HttpResponse(); - $request = new HttpRequest(new HttpUri('')); + $exchanges = \scandir(__DIR__ . '/../Interfaces'); - $request->header->account = 1; - $request->setData('interface', 'OMS'); + if (!\is_dir(__DIR__ . '/temp')) { + \mkdir(__DIR__ . '/temp'); + } - $this->module->apiInterfaceInstall($request, $response); - self::assertEquals(1, $response->get('')['response']->getId()); + foreach ($exchanges as $exchange) { + if (!\is_dir(__DIR__ . '/../Interfaces/' . $exchange) || $exchange === '..' || $exchange === '.') { + continue; + } + + $data = \json_decode(\file_get_contents(__DIR__ . '/../Interfaces/' . $exchange . '/interface.json'), true); + + $response = new HttpResponse(); + $request = new HttpRequest(new HttpUri('')); + + $request->header->account = 1; + $request->setData('title', $data['name']); + $request->setData('export', (bool) $data['export']); + $request->setData('import', (bool) $data['import']); + $request->setData('website', $data['website']); + + $files = []; + + $exchangeFiles = \scandir(__DIR__ . '/../Interfaces/' . $exchange); + foreach ($exchangeFiles as $filePath) { + if ($filePath === '..' || $filePath === '.') { + continue; + } + + if (\is_dir(__DIR__ . '/../Interfaces/' . $exchange . '/' . $filePath)) { + $subdir = \scandir(__DIR__ . '/../Interfaces/' . $exchange . '/' . $filePath); + foreach ($subdir as $subPath) { + if (!\is_file(__DIR__ . '/../Interfaces/' . $exchange . '/' . $filePath . '/' . $subPath)) { + continue; + } + + \copy( + __DIR__ . '/../Interfaces/' . $exchange . '/' . $filePath . '/' . $subPath, + __DIR__ . '/temp/' . $subPath + ); + + $files[] = [ + 'error' => \UPLOAD_ERR_OK, + 'type' => \substr($subPath, \strrpos($subPath, '.') + 1), + 'name' => $filePath . '/' . $subPath, + 'tmp_name' => __DIR__ . '/temp/' . $subPath, + 'size' => \filesize(__DIR__ . '/temp/' . $subPath), + ]; + } + } else { + if (!\is_file(__DIR__ . '/../Interfaces/' . $exchange . '/' . $filePath)) { + continue; + } + + \copy(__DIR__ . '/../Interfaces/' . $exchange . '/' . $filePath, __DIR__ . '/temp/' . $filePath); + + $files[] = [ + 'error' => \UPLOAD_ERR_OK, + 'type' => \substr($filePath, \strrpos($filePath, '.') + 1), + 'name' => $filePath, + 'tmp_name' => __DIR__ . '/temp/' . $filePath, + 'size' => \filesize(__DIR__ . '/temp/' . $filePath), + ]; + } + } + + TestUtils::setMember($request, 'files', $files); + + $this->module->apiInterfaceInstall($request, $response); + self::assertGreaterThan(0, $response->get('')['response']->getId()); + } + + if (\is_dir(__DIR__ . '/temp')) { + \rmdir(__DIR__ . '/temp'); + } } /** diff --git a/tests/Interfaces/OMS/Export/LanguageExport.php b/tests/Interfaces/OMS/Export/LanguageExport.php new file mode 100755 index 0000000..c5424d2 --- /dev/null +++ b/tests/Interfaces/OMS/Export/LanguageExport.php @@ -0,0 +1,13 @@ +exportLanguage(); + } + + /** + * Export data from request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + public function exportFromRequest(RequestAbstract $request) : array + { + $start = new \DateTime($request->getData('start') ?? 'now'); + $end = new \DateTime($request->getData('end') ?? 'now'); + + $this->account = $request->header->account; + + $lang = []; + $lang['Exchange'] = include __DIR__ . '/Lang/' . $request->getLanguage() . '.lang.php'; + + $this->l11n->loadLanguage($request->header->l11n->getLanguage(), 'Exchange', $lang); + + $result = []; + + if ($request->getData('type') === 'language') { + $result = $this->exportLanguage(); + + $log = new ExchangeLog(); + $log->createdBy = $this->account; + $log->setType(ExchangeType::EXPORT); + $log->message = $this->l11n->getText($request->header->l11n->getLanguage(), 'Exchange', '', 'LangFileExported'); + $log->subtype = 'language'; + $log->exchange = (int) $request->getData('id'); + + $result['logs'][] = $log; + } + + return $result; + } + + /** + * Export language + * + * @return array + * + * @since 1.0.0 + */ + public function exportLanguage() : array + { + $languageArray = []; + $supportedLanguages = []; + + $basePath = __DIR__ . '/../../../../../../'; + $modules = \scandir($basePath); + + if ($modules === false) { + return []; // @codeCoverageIgnore + } + + foreach ($modules as $module) { + $themePath = $basePath . $module . '/Theme/'; + + if (!\is_dir($basePath . $module) || $module === '.' || $module === '..' + || !\is_dir($themePath) + ) { + continue; + } + + $module = \trim($module, '/'); + $themes = \scandir($themePath); + + if ($themes === false) { + continue; // @codeCoverageIgnore + } + + foreach ($themes as $theme) { + $theme = \trim($theme, '/'); + $langPath = $themePath . $theme . '/Lang/'; + + if (!\is_dir($themePath . $theme) || $theme === '.' || $theme === '..' + || !\is_dir($langPath) + ) { + continue; + } + + $languages = \scandir($themePath . $theme . '/Lang/'); + if ($languages === false) { + continue; // @codeCoverageIgnore + } + + foreach ($languages as $language) { + if (\stripos($language, '.lang.') === false) { + continue; + } + + $components = \explode('.', $language); + $len = \count($components); + + if ($len === 3 || $len === 4) { + // normal language file + if ($len === 3) { + $supportedLanguages[] = $components[0]; + } elseif ($len === 4) { + $supportedLanguages[] = $components[1]; + } + + $array = include $themePath . $theme . '/Lang/' . $language; + $array = \reset($array); + + if ($array === false) { + continue; // @codeCoverageIgnore + } + + if ($len === 3) { + foreach ($array as $key => $value) { + $languageArray[$module][$theme][''][$key][$components[0]] = $value; + } + } elseif ($len === 4) { + foreach ($array as $key => $value) { + $languageArray[$module][$theme][$components[0]][$key][$components[1]] = $value; + } + } + } + } + } + + // search for translations in tpl files which are not included in the language files + $tplKeys = []; + foreach ($themes as $theme) { + if (!\is_dir($themePath . $theme) || $theme === '.' || $theme === '..') { + continue; + } + + $theme = \trim($theme, '/'); + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($themePath . $theme . '/', \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST + ); + + /** @var \DirectoryIterator $iterator */ + foreach ($iterator as $item) { + if ($item->isDir() || !StringUtils::endsWith($item->getFilename(), '.tpl.php')) { + continue; + } + + $template = \file_get_contents($item->getPathname()); + $keys = []; + + if ($template === false) { + continue; // @codeCoverageIgnore + } + + \preg_match_all('/(\$this\->getHtml\(\')([0-9a-zA-Z:\-]+)(\'\))/', $template, $keys, \PREG_PATTERN_ORDER); + + foreach ($keys[2] ?? [] as $key) { + if (!isset($languageArray[$module][$theme][''][$key])) { + $tplKeys[$module][$theme][''][$key]['en'] = ''; + $languageArray[$module][$theme][''][$key]['en'] = ''; + } + } + } + } + } + + $supportedLanguages = \array_unique($supportedLanguages); + + $content = '"Module";"Theme";"File";"ID";"' . \implode('";"', $supportedLanguages) . '"'; + foreach ($languageArray as $module => $themes) { + foreach ($themes as $theme => $files) { + foreach ($files as $file => $keys) { + foreach ($keys as $key => $value) { + $content .= "\n\"" . $module . '";"' . $theme . '";"' . $file . '";"'; + $content .= ($file === '' && isset($tplKeys[$module][$theme][''][$key]) ? '*' : '') . $key . '"'; + + foreach ($supportedLanguages as $language) { + $content .= ';"' . ($value[$language] ?? '') . '"'; + } + } + } + } + } + + return [ + 'type' => 'file', + 'name' => 'languages.csv', + 'content' => $content, + ]; + } +} diff --git a/tests/Interfaces/OMS/ExporterTest.php b/tests/Interfaces/OMS/ExporterTest.php deleted file mode 100755 index 16a19a5..0000000 --- a/tests/Interfaces/OMS/ExporterTest.php +++ /dev/null @@ -1,55 +0,0 @@ -exporter = new Exporter(new NullConnection(), new L11nManager('Api')); - } - - /** - * @covers Modules\Exchange\Interfaces\OMS\Exporter - * @group module - */ - public function testLanguageExport() : void - { - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - $request->setData('id', '123'); - $request->setData('type', 'language'); - - $export = $this->exporter->exportFromRequest($request); - self::assertCount(4, $export); - self::assertGreaterThan(100, \strlen($export['content'])); - self::assertEquals($export['content'], $this->exporter->export(new \DateTime('now'), new \DateTime('now'))['content']); - } -} diff --git a/tests/Interfaces/OMS/Import/LanguageImport.php b/tests/Interfaces/OMS/Import/LanguageImport.php new file mode 100755 index 0000000..bf01671 --- /dev/null +++ b/tests/Interfaces/OMS/Import/LanguageImport.php @@ -0,0 +1,13 @@ +importLanguage(new HttpRequest()); + } + + /** + * Import data from request + * + * @param RequestAbstract $request Request + * + * @return array + * + * @since 1.0.0 + */ + public function importFromRequest(RequestAbstract $request) : array + { + $start = new \DateTime($request->getData('start') ?? 'now'); + $end = new \DateTime($request->getData('end') ?? 'now'); + + $lang = []; + $lang['Exchange'] = include __DIR__ . '/Lang/' . $request->getLanguage() . '.lang.php'; + + $this->l11n->loadLanguage($request->header->l11n->getLanguage(), 'Exchange', $lang); + + if ($request->getData('db') !== null) { + $this->remote = ConnectionFactory::create([ + 'db' => (string) ($request->getData('db')), + 'host' => (string) ($request->getData('host') ?? ''), + 'port' => (int) ($request->getData('port') ?? 0), + 'database' => (string) ($request->getData('database') ?? ''), + 'login' => (string) ($request->getData('login') ?? ''), + 'password' => (string) ($request->getData('password') ?? ''), + 'datetimeformat' => (string) ($request->getData('datetimeformat') ?? 'Y-m-d H:i:s'), + ]); + + $this->remote->connect(); + + if ($this->remote->getStatus() !== DatabaseStatus::OK) { + return ['status' => false]; + } + } + + $this->account = $request->header->account; + + $result = ['status' => true]; + + if ($request->getData('type') === 'language') { + $this->importLanguage($request); + $log = new ExchangeLog(); + $log->createdBy = $this->account; + $log->setType(ExchangeType::IMPORT); + $log->message = $this->l11n->getText($request->header->l11n->getLanguage(), 'Exchange', '', 'LangFileImported'); + $log->subtype = 'language'; + $log->exchange = (int) $request->getData('id'); + + $result['logs'][] = $log; + } + + return $result; + } + + /** + * Import language + * + * @return void + * + * @since 1.0.0 + */ + public function importLanguage(RequestAbstract $request) : void + { + $upload = ApiController::uploadFilesToDestination($request->getFiles()); + + $fp = \fopen($upload['file0']['path'] . '/' . $upload['file0']['filename'], 'r'); + if ($fp === false) { + return; // @codeCoverageIgnore + } + + $header = \fgetcsv($fp, 0, ';', '"'); + + if ($header === false) { + return; // @codeCoverageIgnore + } + + $languageArray = []; + $supportedLanguages = \array_slice($header, 4); + $keyLengths = []; + + while (($line = \fgetcsv($fp, 0, ';', '"')) !== false) { + $translations = \array_slice($line, 4); + + $line[0] = \trim($line[0]); + $line[1] = \trim($line[1]); + $line[2] = \trim($line[2]); + $line[3] = \trim($line[3]); + + if (($keyLengths[$line[0]][$line[1]][$line[2]] ?? 0) < \strlen($line[3])) { + $keyLengths[$line[0]][$line[1]][$line[2]] = \strlen($line[3]); + } + + foreach ($supportedLanguages as $index => $language) { + if (empty(\trim($language))) { + continue; // @codeCoverageIgnore + } + + $languageArray[$line[0]][$line[1]][$line[2]][$line[3]][\trim($language)] = $translations[$index]; + } + } + + \fclose($fp); + \unlink($upload['file0']['path'] . '/' . $upload['file0']['filename']); + + foreach ($languageArray as $module => $themes) { + foreach ($themes as $theme => $files) { + foreach ($files as $file => $keys) { + foreach ($supportedLanguages as $language) { + $langFile = __DIR__ . '/../../../../../../' + . $module . '/Theme/' + . $theme . '/Lang/' + . ($file === '' ? '' : $file . '.') + . \trim($language) + . '.lang.php'; + + if (\is_file($langFile)) { + \unlink($langFile); + } + + $fp = \fopen($langFile, 'w+'); + if ($fp === false) { + continue; // @codeCoverageIgnore + } + + \fwrite($fp, + " [\n" + ); + + \ksort($keys); + + foreach ($keys as $key => $values) { + $key = \ltrim($key, '*'); + + \fwrite($fp, + " '" . $key . "'" + . \str_repeat(' ', $keyLengths[$module][$theme][$file] - \strlen($key)) + . " => '" + . \str_replace(['\\', '\''], ['\\\\', '\\\''], $values[$language] ?? '') + . "',\n" + ); + } + + \fwrite($fp, "]];\n"); + \fclose($fp); + } + } + } + } + } +} diff --git a/tests/Interfaces/OMS/ImporterTest.php b/tests/Interfaces/OMS/ImporterTest.php deleted file mode 100755 index 9b28083..0000000 --- a/tests/Interfaces/OMS/ImporterTest.php +++ /dev/null @@ -1,71 +0,0 @@ -importer = new Importer(new NullConnection(), new NullConnection(), new L11nManager('Api')); - } - - /** - * @covers Modules\Exchange\Interfaces\OMS\Importer - * @group module - */ - public function testLanguageImport() : void - { - $request = new HttpRequest(new HttpUri('')); - - $request->header->account = 1; - $request->setData('id', '123'); - $request->setData('type', 'language'); - - if (!\is_file(__DIR__ . '/test_tmp.csv')) { - \copy(__DIR__ . '/test.csv', __DIR__ . '/test_tmp.csv'); - } - - TestUtils::setMember($request, 'files', [ - 'file0' => [ - 'name' => 'test.csv', - 'type' => 'csv', - 'tmp_name' => __DIR__ . '/test_tmp.csv', - 'error' => \UPLOAD_ERR_OK, - 'size' => \filesize(__DIR__ . '/test_tmp.csv'), - ], - ]); - - $export = $this->importer->importFromRequest($request); - self::assertEquals( - \date('Y-m-d'), - \date('Y-m-d', \filemtime(__DIR__ . '/../../../../TestModule/Theme/Backend/Lang/en.lang.php')) - ); - } -} diff --git a/tests/Interfaces/OMS/Lang/en.lang.php b/tests/Interfaces/OMS/Lang/en.lang.php new file mode 100755 index 0000000..3c964e7 --- /dev/null +++ b/tests/Interfaces/OMS/Lang/en.lang.php @@ -0,0 +1,19 @@ + 'Language', + 'LangFileExported' => 'Language file got exported', + 'LangFileImported' => 'Language file got imported', +]; diff --git a/tests/Interfaces/OMS/export.tpl.php b/tests/Interfaces/OMS/export.tpl.php new file mode 100755 index 0000000..6f82cdf --- /dev/null +++ b/tests/Interfaces/OMS/export.tpl.php @@ -0,0 +1,30 @@ +getData('lang'); +?> +
+
+
+
+
printHtml($lang['Language']); ?> - OMS
+
+
+
+ +
+
+
+
+
diff --git a/tests/Interfaces/OMS/import.tpl.php b/tests/Interfaces/OMS/import.tpl.php new file mode 100755 index 0000000..e212afe --- /dev/null +++ b/tests/Interfaces/OMS/import.tpl.php @@ -0,0 +1,35 @@ +getData('lang'); +?> +
+
+
+
+
printHtml($lang['Language']); ?> - OMS
+
+ + +
+
+
+
+
+ +
+
+
+
+
diff --git a/tests/Interfaces/OMS/interface.json b/tests/Interfaces/OMS/interface.json new file mode 100755 index 0000000..968e18a --- /dev/null +++ b/tests/Interfaces/OMS/interface.json @@ -0,0 +1,7 @@ +{ + "name": "OMS", + "version": "1.0.0", + "website": "", + "export": true, + "import": true +} \ No newline at end of file diff --git a/tests/Interfaces/OMS/test.csv b/tests/Interfaces/OMS/test.csv deleted file mode 100755 index 4778853..0000000 --- a/tests/Interfaces/OMS/test.csv +++ /dev/null @@ -1,3 +0,0 @@ -"Module";"Theme";"File";"ID";"en";"ar";"cs";"da";"de";"el";"es";"fi";"fr";"hu";"it";"ja";"ko";"no";"pl";"pt";"ru";"sv";"th";"tr";"uk";"zh" -"TestModule";"Backend";"";"Test1";"Test1";"TEST1";"test1";"test1";"";"test1";"test1";"test1";"test1";"test1";"test1";"Test1を";"TEST1";"test1";"test1";"Test1";"Test1";"test1";"test1";"Test1";"Test1";"测试1" -"TestModule";"Backend";"";"Test2";"Test2";"TEST2";"test2";"test2";"";"test2";"test2";"test2";"test2";"test2";"test2";"Test2を";"TEST2";"test2";"test2";"test2";"Test2";"test2";"Test2";"Test2";"Test2";"TEST2" \ No newline at end of file diff --git a/tests/Models/ExporterAbstractTest.php b/tests/Models/ExporterAbstractTest.php index 4adb20b..92090d2 100755 --- a/tests/Models/ExporterAbstractTest.php +++ b/tests/Models/ExporterAbstractTest.php @@ -15,6 +15,7 @@ declare(strict_types=1); namespace Modules\Exchange\tests\Models; use Modules\Exchange\Models\ExporterAbstract; +use phpOMS\DataStorage\Database\Connection\NullConnection; use phpOMS\DataStorage\Database\Connection\SQLiteConnection; use phpOMS\Localization\L11nManager; use phpOMS\Message\Http\HttpRequest; @@ -35,6 +36,7 @@ final class ExporterAbstractTest extends \PHPUnit\Framework\TestCase { $this->class = new class( new SQLiteConnection($GLOBALS['CONFIG']['db']['core']['sqlite']['admin']), + new NullConnection(), new L11nManager('placeholder') ) extends ExporterAbstract { public function exportFromRequest(RequestAbstract $request) : array diff --git a/tests/Models/InterfaceManagerTest.php b/tests/Models/InterfaceManagerTest.php index 7d81011..20ee145 100755 --- a/tests/Models/InterfaceManagerTest.php +++ b/tests/Models/InterfaceManagerTest.php @@ -39,82 +39,9 @@ final class InterfaceManagerTest extends \PHPUnit\Framework\TestCase { self::assertEquals(0, $this->interface->getId()); self::assertEquals('', $this->interface->getPath()); - self::assertEquals('', $this->interface->getName()); - self::assertEquals('', $this->interface->getInterfacePath()); - self::assertFalse($this->interface->hasImport()); - self::assertFalse($this->interface->hasExport()); - self::assertEquals([], $this->interface->get()); - } - - /** - * @covers Modules\Exchange\Models\InterfaceManager - * @group module - */ - public function testLoadInputOutput() : void - { - $this->interface = new InterfaceManager(__DIR__ . '/testInterface.json'); - $this->interface->load(); - - self::assertEquals( - \json_decode(\file_get_contents(__DIR__ . '/testInterface.json'), true), - $this->interface->get() - ); - } - - /** - * @covers Modules\Exchange\Models\InterfaceManager - * @group module - */ - public function testSetInputOutput() : void - { - $this->interface = new InterfaceManager(__DIR__ . '/testInterface.json'); - $this->interface->load(); - - $this->interface->set('website', 'https://karaka.app'); - self::assertEquals('https://karaka.app', $this->interface->get()['website']); - - self::assertNotEquals( - \json_decode(\file_get_contents(__DIR__ . '/testInterface.json'), true), - $this->interface->get() - ); - - $this->interface->update(); - self::assertEquals( - \json_decode(\file_get_contents(__DIR__ . '/testInterface.json'), true), - $this->interface->get() - ); - - $this->interface->set('website', ''); - $this->interface->update(); - } - - /** - * @covers Modules\Exchange\Models\InterfaceManager - * @group module - */ - public function testInvalidPathLoad() : void - { - $this->expectException(\phpOMS\System\File\PathException::class); - $this->interface->load(); - } - - /** - * @covers Modules\Exchange\Models\InterfaceManager - * @group module - */ - public function testInvalidPathUpdate() : void - { - $this->expectException(\phpOMS\System\File\PathException::class); - $this->interface->update(); - } - - /** - * @covers Modules\Exchange\Models\InterfaceManager - * @group module - */ - public function testInvalidDataSet() : void - { - $this->expectException(\InvalidArgumentException::class); - $this->interface->set('test/path', new InterfaceManager()); + self::assertEquals('', $this->interface->title); + self::assertFalse($this->interface->hasImport); + self::assertFalse($this->interface->hasExport); + self::assertEquals([], $this->interface->getSettings()); } }