diff --git a/.travis.yml b/.travis.yml index 3530a8c..5f7df5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,14 @@ addons: language: php matrix: include: + - php: 7.2 + env: + - MAGENTO_VERSION=2.2 + - TEST_SUITE=unit + - php: 7.3 + env: + - MAGENTO_VERSION=2.3-develop + - TEST_SUITE=unit - php: 7.2 env: - MAGENTO_VERSION=2.2 diff --git a/.travis/before_script.sh b/.travis/before_script.sh index 154ddc6..d8d55d4 100755 --- a/.travis/before_script.sh +++ b/.travis/before_script.sh @@ -25,8 +25,13 @@ cd magento2 composer config minimum-stability dev composer config repositories.travis_to_test git https://github.com/$TRAVIS_REPO_SLUG.git #TODO make it work with tags as well: + composer require ${COMPOSER_PACKAGE_NAME}:dev-${TRAVIS_BRANCH}\#{$TRAVIS_COMMIT} +# Install dev dependencies of module +php ../.travis/merge-dev.php vendor/$COMPOSER_PACKAGE_NAME/composer.json composer.json +composer update + # prepare for test suite case $TEST_SUITE in integration) diff --git a/.travis/merge-dev.php b/.travis/merge-dev.php new file mode 100644 index 0000000..8f411c3 --- /dev/null +++ b/.travis/merge-dev.php @@ -0,0 +1,23 @@ + $value) { + $pathPrefix = dirname($fromFile) . DIRECTORY_SEPARATOR; + $fromJson['autoload-dev']['psr-4'][$key] = $pathPrefix . $value; +} + +$toJson['require-dev'] = array_replace_recursive($toJson['require-dev'] ?? [], $fromJson['require-dev']); +$toJson['autoload-dev'] = array_merge_recursive($toJson['autoload-dev'] ?? [], $fromJson['autoload-dev']); + +file_put_contents($toFile, json_encode($toJson, JSON_PRETTY_PRINT)); \ No newline at end of file diff --git a/composer.json b/composer.json index 656f1ed..390bd6c 100755 --- a/composer.json +++ b/composer.json @@ -26,10 +26,15 @@ "src/registration.php" ], "psr-4": { - "IntegerNet\\AsyncVarnish\\Test\\": "tests/", "IntegerNet\\AsyncVarnish\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "IntegerNet\\AsyncVarnish\\Test\\": "tests/", + "IntegerNet\\AsyncVarnish\\": "tests/src/" + } + }, "repositories": [ { "type": "composer", @@ -37,6 +42,6 @@ } ], "require-dev": { - "magento/magento-coding-standard": "@dev" + "react/http": "0.8.5" } } diff --git a/src/Api/TagRepositoryInterface.php b/src/Api/TagRepositoryInterface.php new file mode 100644 index 0000000..0e9048c --- /dev/null +++ b/src/Api/TagRepositoryInterface.php @@ -0,0 +1,37 @@ +tagRepository->getAll(); $maxHeaderLength = $this->getMaxHeaderLengthFromConfig(); diff --git a/src/Model/ResourceModel/Tag.php b/src/Model/ResourceModel/Tag.php index f3120eb..1daab7b 100644 --- a/src/Model/ResourceModel/Tag.php +++ b/src/Model/ResourceModel/Tag.php @@ -22,11 +22,10 @@ public function getMaxTagId(int $limit):array $connection = $this->getConnection(); $subSetSelect = $connection->select()->from( - self::TABLE_NAME, + $this->getTable(self::TABLE_NAME), ['entity_id','tag'] )->order( - 'entity_id', - 'ASC' + 'entity_id ASC' )->limit( $limit ); @@ -44,7 +43,7 @@ public function getUniqueTagsByMaxId(int $maxId):array $connection = $this->getConnection(); $select = $connection->select()->from( - ['main_table' => self::TABLE_NAME], + ['main_table' => $this->getTable(self::TABLE_NAME)], ['tag'] )->group( 'tag' diff --git a/src/Model/TagRepository.php b/src/Model/TagRepository.php index b556291..c1229d6 100644 --- a/src/Model/TagRepository.php +++ b/src/Model/TagRepository.php @@ -3,11 +3,12 @@ namespace IntegerNet\AsyncVarnish\Model; +use IntegerNet\AsyncVarnish\Api\TagRepositoryInterface; use Magento\Framework\App\ResourceConnection; use IntegerNet\AsyncVarnish\Model\ResourceModel\Tag as TagResource; use Magento\Framework\App\Config\ScopeConfigInterface; -class TagRepository +class TagRepository implements TagRepositoryInterface { /** * DB Storage table name @@ -19,6 +20,9 @@ class TagRepository */ const FETCH_TAG_LIMIT_CONFIG_PATH = 'system/full_page_cache/async_varnish/varnish_fetch_tag_limit'; + /** + * @var int|null + */ private $lastUsedId; /** @@ -52,9 +56,9 @@ public function __construct( $this->scopeConfig = $scopeConfig; } - private function getTagFetchLimit() + private function getTagFetchLimit(): int { - return $this->scopeConfig->getValue(self::FETCH_TAG_LIMIT_CONFIG_PATH); + return (int) $this->scopeConfig->getValue(self::FETCH_TAG_LIMIT_CONFIG_PATH); } /** @@ -64,7 +68,7 @@ private function getTagFetchLimit() * @return int * @throws \Exception */ - public function insertMultiple($tags = []) + public function insertMultiple($tags = []): int { if (empty($tags)) { return 0; @@ -92,7 +96,7 @@ function ($tag) { * @return int * @throws \Exception */ - public function deleteUpToId($maxId = 0) + public function deleteUpToId(int $maxId = 0): int { try { $tableName = $this->resource->getTableName(self::TABLE_NAME); @@ -103,10 +107,9 @@ public function deleteUpToId($maxId = 0) } /** - * @return array * @throws \Zend_Db_Statement_Exception */ - public function getAll() + public function getAll(): array { $tags = []; @@ -120,24 +123,21 @@ public function getAll() return $tags; } - $maxId = $maxIdResult['max_id']; + $maxId = (int)$maxIdResult['max_id']; - $uniqueTagsResult = $tagResource->getUniqueTagsByMaxId((int)$maxId); + $uniqueTagsResult = $tagResource->getUniqueTagsByMaxId($maxId); if (!empty($uniqueTagsResult)) { $this->lastUsedId = $maxId; foreach ($uniqueTagsResult as $tag) { - $tags[] = ($tag['tag']); + $tags[] = $tag['tag']; } } return $tags; } - /** - * @return int - */ - public function getLastUsedId() + public function getLastUsedId(): int { return $this->lastUsedId ?: 0; } diff --git a/src/etc/di.xml b/src/etc/di.xml index 6e6a05b..5673de7 100644 --- a/src/etc/di.xml +++ b/src/etc/di.xml @@ -12,4 +12,6 @@ + \ No newline at end of file diff --git a/tests/Integration/AbstractTagRepositoryTest.php b/tests/Integration/AbstractTagRepositoryTest.php new file mode 100644 index 0000000..1a13dc9 --- /dev/null +++ b/tests/Integration/AbstractTagRepositoryTest.php @@ -0,0 +1,78 @@ +tagRepository = $this->getTestSubject(); + } + + abstract protected function getTestSubject(): TagRepositoryInterface; + + public function testInsertAndRetrieve() + { + $affected = $this->tagRepository->insertMultiple(['x', 'y', 'z']); + $this->assertEquals(3, $affected, 'insertMultiple() should return number of inserted rows'); + $this->assertEqualsCanonicalizing(['x', 'y', 'z'], $this->tagRepository->getAll()); + } + + public function testNoDuplicatesAreRetrieved() + { + $affected = $this->tagRepository->insertMultiple(['x', 'y', 'x']); + $this->assertEquals(3, $affected, 'insertMultiple() should return number of inserted rows'); + $this->assertEqualsCanonicalizing(['x', 'y'], $this->tagRepository->getAll()); + } + + public function testNoDuplicatesAreRetrievedAfterSubsequentCalls() + { + $affected = $this->tagRepository->insertMultiple(['x', 'y']); + $this->assertEquals(2, $affected, 'insertMultiple() should return number of inserted rows'); + $affected = $this->tagRepository->insertMultiple(['y', 'z']); + $this->assertEquals(2, $affected, 'insertMultiple() should return number of inserted rows'); + $this->assertEqualsCanonicalizing(['x', 'y', 'z'], $this->tagRepository->getAll()); + } + + public function testLastUsedIdIncreases() + { + $this->tagRepository->insertMultiple(['x']); + $this->tagRepository->getAll(); + $lastUsedId = $this->tagRepository->getLastUsedId(); + $this->tagRepository->insertMultiple(['y']); + $this->tagRepository->getAll(); + //TODO maybe throw exception if getAll has not been called before: + $this->assertEquals($lastUsedId + 1, $this->tagRepository->getLastUsedId()); + } + + public function testDeleteUpToId() + { + $this->tagRepository->insertMultiple(['x', 'y', 'z']); + $this->tagRepository->getAll(); + $lastUsedId = $this->tagRepository->getLastUsedId(); + $this->tagRepository->insertMultiple(['a', 'b', 'c']); + $affected = $this->tagRepository->deleteUpToId($lastUsedId); + $this->assertEquals(3, $affected, 'deleteUpToId() should return number of deleted rows'); + $this->assertEqualsCanonicalizing(['a', 'b', 'c'], $this->tagRepository->getAll()); + } + + /** + * Backport from PHPUnit 8 + * + * @param array $expected + * @param array $actual + */ + public static function assertEqualsCanonicalizing(array $expected, array $actual, string $message = '') + { + self::assertEquals($expected, $actual, $message, 0.0, 10, true); + } +} diff --git a/tests/Integration/ModuleTest.php b/tests/Integration/ModuleTest.php new file mode 100644 index 0000000..865824e --- /dev/null +++ b/tests/Integration/ModuleTest.php @@ -0,0 +1,44 @@ +objectManager->create(ModuleList::class); + return $moduleList; + } + protected function setUp() + { + $this->objectManager = ObjectManager::getInstance(); + } + public function testTheModuleIsRegistered() + { + $registrar = new ComponentRegistrar(); + $paths = $registrar->getPaths(ComponentRegistrar::MODULE); + $this->assertArrayHasKey(self::MODULE_NAME, $paths, 'Module should be registered'); + } + public function testTheModuleIsKnownAndEnabled() + { + $moduleList = $this->getTestModuleList(); + $this->assertTrue($moduleList->has(self::MODULE_NAME), 'Module should be enabled'); + } + +} \ No newline at end of file diff --git a/tests/Integration/PurgeCacheTest.php b/tests/Integration/PurgeCacheTest.php new file mode 100644 index 0000000..5caa888 --- /dev/null +++ b/tests/Integration/PurgeCacheTest.php @@ -0,0 +1,135 @@ +startMockServer(); + $this->createRequestLog(); + + $this->configureVarnishHost(); + $this->purgeCache = Bootstrap::getObjectManager()->get(PurgeCache::class); + } + + private function configureVarnishHost() + { + /** @var ObjectManager $objectManager */ + $objectManager = Bootstrap::getObjectManager(); + $deploymentConfig = new DeploymentConfig( + $objectManager->get(DeploymentConfig\Reader::class), + ['http_cache_hosts' => [['host' => '127.0.0.1', 'port' => self::MOCK_SERVER_PORT]]] + ); + $objectManager->addSharedInstance( + $deploymentConfig, + DeploymentConfig::class + ); + } + + protected function tearDown() + { + $this->stopMockServer(); + } + + public function testWebserver() + { + $this->assertEquals("OK\n", \file_get_contents('http://127.0.0.1:' . self::MOCK_SERVER_PORT . '/')); + } + + public function testPurgeRequestIsSentToVarnish() + { + $tagsPattern = 'XXX|YYY|ZZZZ'; + $result = $this->purgeCache->sendPurgeRequest($tagsPattern); + $this->assertTrue($result); + $this->assertEquals( + [ + [ + 'method' => 'PURGE', + 'headers' => ['Host' => ['127.0.0.1'], 'X-Magento-Tags-Pattern' => [$tagsPattern]], + ], + ], + $this->getRequestsFromLog() + ); + } + + private function startMockServer(): void + { + $objectManager = Bootstrap::getObjectManager(); + /** @var PhpExecutableFinder $phpExecutableFinder */ + $phpExecutableFinder = $objectManager->get(PhpExecutableFinder::class); + $mockServerCmd = $phpExecutableFinder->find() . ' ' . self::MOCK_SERVER_DIR . '/server.php'; + //the following needs Symfony Process >= 4.2.0 +// $this->mockServerProcess = Process::fromShellCommandline($mockServerCmd); + //so we use the old way to instantiate Process from string: + $this->mockServerProcess = new Process($mockServerCmd); + $this->mockServerProcess->start(); + //the following needs Symfony Process >= 4.2.0 +// $this->mockServerProcess->waitUntil( +// function($output) { +// return $output === 'Started'; +// } +// ); + // so we wait a second or two instead: + sleep(2); + } + + private function stopMockServer(): void + { + // issue: this only kills the parent shell script, not the PHP process (Symfony Process 4.1) +// $this->mockServerProcess->stop(); + // so we implemented a kill switch in the server: + $ch = \curl_init('http://127.0.0.1:8082/?kill=1'); + \curl_exec($ch); + } + + private function createRequestLog(): void + { + \file_put_contents(self::REQUEST_LOG_FILE, ''); + \chmod(self::REQUEST_LOG_FILE, 0666); + } + + private function getRequestsFromLog(): array + { + $requests = \array_map( + function (string $line): array { + return \json_decode($line, true); + }, + \file(self::REQUEST_LOG_FILE) + ); + return $requests; + } +} diff --git a/tests/Integration/TagRepositoryTest.php b/tests/Integration/TagRepositoryTest.php new file mode 100644 index 0000000..662baa0 --- /dev/null +++ b/tests/Integration/TagRepositoryTest.php @@ -0,0 +1,21 @@ +get(TagRepository::class); + } +} diff --git a/tests/Integration/VarnishMock/.gitignore b/tests/Integration/VarnishMock/.gitignore new file mode 100644 index 0000000..89267c3 --- /dev/null +++ b/tests/Integration/VarnishMock/.gitignore @@ -0,0 +1 @@ +/.requests.log \ No newline at end of file diff --git a/tests/Integration/VarnishMock/server.php b/tests/Integration/VarnishMock/server.php new file mode 100644 index 0000000..17a1dc9 --- /dev/null +++ b/tests/Integration/VarnishMock/server.php @@ -0,0 +1,38 @@ +getQueryParams()['kill'] ?? false) { + exit; + } + $requestJson = \json_encode( + [ + 'method' => $request->getMethod(), + 'headers' => $request->getHeaders() + ] + ); + \file_put_contents(__DIR__ . '/.requests.log', $requestJson . "\n", FILE_APPEND); + + return new \React\Http\Response( + 200, + array( + 'Content-Type' => 'text/plain' + ), + "OK\n" + ); +}); + +$socket = new \React\Socket\Server(8082, $loop); +$server->listen($socket); + +echo "Started"; + +$loop->run(); diff --git a/tests/Unit/FakeTagRepositoryTest.php b/tests/Unit/FakeTagRepositoryTest.php new file mode 100644 index 0000000..be3743a --- /dev/null +++ b/tests/Unit/FakeTagRepositoryTest.php @@ -0,0 +1,17 @@ + + + + ../../../vendor/integer-net/magento2-async-varnish/tests/Unit + + + + + + + + ../../src/app/code/* + + ../../src/app/code/*/*/Test + + + + + + + + + + + + + diff --git a/tests/src/FakeTagRepository.php b/tests/src/FakeTagRepository.php new file mode 100644 index 0000000..229283e --- /dev/null +++ b/tests/src/FakeTagRepository.php @@ -0,0 +1,57 @@ +tags = array_merge($this->tags, $tags); + return count($tags); + } + + public function deleteUpToId(int $maxId = 0): int + { + $deleted = 0; + foreach ($this->tags as $key => $tag) { + if ($key <= $maxId) { + unset($this->tags[$key]); + ++$deleted; + } + } + return $deleted; + } + + public function getAll(): array + { + return array_unique($this->tags); + } + + public function getLastUsedId(): int + { + return array_key_last($this->tags); + } + +} + +/* + * PHP 7.2 Polyfill + */ +if (!\function_exists('array_key_last')) { + function array_key_last(array $array) + { + if (empty($array)) { + return null; + } + + return key(array_slice($array, -1, 1, true)); + } +} \ No newline at end of file