diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index bbc30f485..bd39a76c8 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -21,23 +21,31 @@ public function getRules() : array { 'phpdoc_single_line_var_spacing' => true, 'phpdoc_var_without_name' => true ]; - return array_merge(['@PSR12' => true], $parentRules, $additionalRules); + $ret = array_merge(['@PSR12' => true], $parentRules, $additionalRules); + // print_r($ret); + return $ret; } } $config = new CookbookConfig(); -$config +$finder = $config ->getFinder() - ->ignoreVCSIgnored(true) + // ->ignoreVCSIgnored(true) ->exclude('build') ->exclude('l10n') -// ->notPath('lib/Vendor') ->exclude('src') ->exclude('node_modules') ->exclude('vendor') ->exclude('.github') - ->in(__DIR__); + ->in(__DIR__ ); +$config->setFinder($finder); + +// foreach($finder as $f) { +// print_r($f); +// } + +// print_r($config); return $config; diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fbdc8fb5..da55a617b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## [Unreleased] +### Fixed +- Prevent slow loading of recipes due to iteration over all files + [#1072](https://github.com/nextcloud/cookbook/pull/1072) @christianlupus + + ## 0.9.13 - 2022-07-02 ### Added diff --git a/cookbook.code-workspace b/cookbook.code-workspace index c2955098a..1615d9dac 100644 --- a/cookbook.code-workspace +++ b/cookbook.code-workspace @@ -40,13 +40,21 @@ "**/vendor/**/{Tests,tests}/**", "**/.history/**", "**/vendor/**/vendor/**", - "3rdparty/**" + // "3rdparty/**" ], "cSpell.words": [ "Nextcloud" ], "path-intellisense.mappings": { "cookbook": "${workspaceFolder}/src", - } + }, + "editor.quickSuggestions": { + "other": "on", + "comments": "on", + "strings": "on" + }, + "intelephense.environment.includePaths": [ + "${workspaceFolder:base}/3rdparty/doctrine/dbal/src" + ] } } diff --git a/lib/Db/DbTypesPolyfillHelper.php b/lib/Db/DbTypesPolyfillHelper.php index 1d294cf89..8262e0bcc 100644 --- a/lib/Db/DbTypesPolyfillHelper.php +++ b/lib/Db/DbTypesPolyfillHelper.php @@ -14,6 +14,9 @@ class DbTypesPolyfillHelper { */ private $string; + /** @var string */ + private $date; + public function __construct(Util $util) { switch ($util->getVersion()[0]) { case 18: @@ -21,11 +24,13 @@ public function __construct(Util $util) { case 20: $this->int = \Doctrine\DBAL\Types\Type::INTEGER; $this->string = \Doctrine\DBAL\Types\Type::STRING; + $this->date = \Doctrine\DBAL\Types\Type::DATE; break; default: $this->int = \OCP\DB\Types::INTEGER; $this->string = \OCP\DB\Types::STRING; + $this->date = \OCP\DB\Types::DATE; break; } } @@ -43,4 +48,11 @@ final public function INT() { final public function STRING() { return $this->string; } + + /** + * @return string + */ + final public function DATE() { + return $this->date; + } } diff --git a/lib/Db/RecipeDb.php b/lib/Db/RecipeDb.php index cb01d1078..a0d15bfab 100755 --- a/lib/Db/RecipeDb.php +++ b/lib/Db/RecipeDb.php @@ -2,6 +2,7 @@ namespace OCA\Cookbook\Db; +use DateTimeImmutable; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\AppFramework\Db\DoesNotExistException; @@ -58,6 +59,8 @@ public function findRecipeById(int $id) { $ret = []; $ret['name'] = $row['name']; $ret['id'] = $row['recipe_id']; + $ret['dateCreated'] = $row['date_created']; + $ret['dateModified'] = $row['date_modified']; return $ret; } @@ -91,7 +94,7 @@ public function deleteRecipeById(int $id) { public function findAllRecipes(string $user_id) { $qb = $this->db->getQueryBuilder(); - $qb->select(['r.recipe_id', 'r.name', 'k.name AS keywords']) + $qb->select(['r.recipe_id', 'r.name', 'r.date_created', 'r.date_modified', 'k.name AS keywords', 'c.name AS category']) ->from(self::DB_TABLE_RECIPES, 'r') ->where('r.user_id = :user') ->orderBy('r.name'); @@ -102,11 +105,19 @@ public function findAllRecipes(string $user_id) { 'k.user_id = :user' ) ); + $qb->leftJoin('r', self::DB_TABLE_CATEGORIES, 'c', + $qb->expr()->andX( + 'c.recipe_id = r.recipe_id', + 'c.user_id = r.user_id' + ) + ); $cursor = $qb->execute(); $result = $cursor->fetchAll(); $cursor->closeCursor(); + $result = $this->mapDbNames($result); + $result = $this->sortRecipes($result); // group recipes, convert keywords to comma-separated list @@ -115,6 +126,17 @@ public function findAllRecipes(string $user_id) { return $this->unique($recipesGroupedTags); } + private function mapDbNames($results) { + return array_map(function ($x) { + $x['dateCreated'] = $x['date_created']; + $x['dateModified'] = $x['date_modified']; + unset($x['date_created']); + unset($x['date_modified']); + + return $x; + }, $results); + } + public function unique(array $result) { // NOTE: This post processing shouldn't be necessary // When sharing recipes with other users, they are occasionally returned twice @@ -197,7 +219,7 @@ public function findAllCategories(string $user_id) { ) ) ->where( - $qb->expr()->eq('r.user_id', $qb->expr()->literal($user_id)), + $qb->expr()->eq('r.user_id', $qb->createNamedParameter($user_id, IQueryBuilder::PARAM_STR)), $qb->expr()->isNull('c.name') ); @@ -229,7 +251,7 @@ public function getRecipesByCategory(string $category, string $user_id) { // for the recipe, but those don't seem to work: // $qb->select(['r.recipe_id', 'r.name', 'GROUP_CONCAT(k.name) AS keywords']) // not working // $qb->select(['r.recipe_id', 'r.name', DB::raw('GROUP_CONCAT(k.name) AS keywords')]) // not working - $qb->select(['r.recipe_id', 'r.name', 'k.name AS keywords']) + $qb->select(['r.recipe_id', 'r.name', 'r.date_created', 'r.date_modified', 'k.name AS keywords']) ->from(self::DB_TABLE_CATEGORIES, 'c') ->where('c.name = :category') ->andWhere('c.user_id = :user') @@ -242,7 +264,7 @@ public function getRecipesByCategory(string $category, string $user_id) { $qb->groupBy(['r.name', 'r.recipe_id', 'k.name']); $qb->orderBy('r.name'); } else { - $qb->select(['r.recipe_id', 'r.name', 'k.name AS keywords']) + $qb->select(['r.recipe_id', 'r.name', 'r.date_created', 'r.date_modified', 'k.name AS keywords']) ->from(self::DB_TABLE_RECIPES, 'r') ->leftJoin('r', self::DB_TABLE_KEYWORDS, 'k', 'r.recipe_id = k.recipe_id') ->leftJoin( @@ -255,7 +277,7 @@ public function getRecipesByCategory(string $category, string $user_id) { ) ) ->where( - $qb->expr()->eq('r.user_id', $qb->expr()->literal($user_id)), + $qb->expr()->eq('r.user_id', $qb->createNamedParameter($user_id, IQueryBuilder::PARAM_STR)), $qb->expr()->isNull('c.name') ); } @@ -264,6 +286,8 @@ public function getRecipesByCategory(string $category, string $user_id) { $result = $cursor->fetchAll(); $cursor->closeCursor(); + $result = $this->mapDbNames($result); + // group recipes, convert keywords to comma-separated list $recipesGroupedTags = $this->groupKeywordInResult($result); @@ -279,7 +303,7 @@ public function getRecipesByKeywords(string $keywords, string $user_id) { $qb = $this->db->getQueryBuilder(); - $qb->select(['r.recipe_id', 'r.name', 'kk.name AS keywords']) + $qb->select(['r.recipe_id', 'r.name', 'r.date_created', 'r.date_modified', 'kk.name AS keywords']) ->from(self::DB_TABLE_KEYWORDS, 'k') ->where('k.name IN (:keywords)') ->andWhere('k.user_id = :user') @@ -296,6 +320,8 @@ public function getRecipesByKeywords(string $keywords, string $user_id) { $result = $cursor->fetchAll(); $cursor->closeCursor(); + $result = $this->mapDbNames($result); + // group recipes, convert keywords to comma-separated list $recipesGroupedTags = $this->groupKeywordInResult($result); @@ -314,7 +340,7 @@ public function findRecipes(array $keywords, string $user_id) { $qb = $this->db->getQueryBuilder(); - $qb->select(['r.recipe_id', 'r.name', 'k.name AS keywords']) + $qb->select(['r.recipe_id', 'r.name', 'r.date_created', 'r.date_modified', 'k.name AS keywords', 'c.name AS category']) ->from(self::DB_TABLE_RECIPES, 'r'); $qb->leftJoin('r', self::DB_TABLE_KEYWORDS, 'k', 'k.recipe_id = r.recipe_id'); @@ -348,6 +374,8 @@ public function findRecipes(array $keywords, string $user_id) { $result = $cursor->fetchAll(); $cursor->closeCursor(); + $result = $this->mapDbNames($result); + // group recipes, convert keywords to comma-separated list $recipesGroupedTags = $this->groupKeywordInResult($result); @@ -432,8 +460,8 @@ public function deleteRecipes(array $ids, string $userId) { foreach ($ids as $id) { $qb->orWhere( $qb->expr()->andX( - "recipe_id = $id", - $qb->expr()->eq("user_id", $qb->expr()->literal($userId)) + $qb->expr()->eq('recipe_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)), + $qb->expr()->eq("user_id", $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) )); } @@ -454,7 +482,9 @@ public function insertRecipes(array $recipes, string $userId) { ->values([ 'recipe_id' => ':id', 'user_id' => ':userid', - 'name' => ':name' + 'name' => ':name', + 'date_created' => ':dateCreated', + 'date_modified' => ':dateModified', ]); $qb->setParameter('userid', $userId); @@ -463,6 +493,12 @@ public function insertRecipes(array $recipes, string $userId) { $qb->setParameter('id', $recipe['id'], $this->types->INT()); $qb->setParameter('name', $recipe['name'], $this->types->STRING()); + $dateCreated = $this->parseDate($recipe['dateCreated']); + $qb->setParameter('dateCreated', $dateCreated, $this->types->DATE()); + + $dateModified = $this->parseDate($recipe['dateModified']); + $qb->setParameter('dateModified', $dateModified, $this->types->DATE()); + $qb->execute(); } } @@ -472,20 +508,27 @@ public function updateRecipes(array $recipes, string $userId) { return; } - $qb = $this->db->getQueryBuilder(); - foreach ($recipes as $recipe) { + $qb = $this->db->getQueryBuilder(); $qb->update(self::DB_TABLE_RECIPES) ->where('recipe_id = :id', 'user_id = :uid'); - $literal = []; - $literal['name'] = $qb->expr()->literal($recipe['name'], IQueryBuilder::PARAM_STR); - $qb->set('name', $literal['name']); + $qb->set('name', $qb->createNamedParameter($recipe['name'], IQueryBuilder::PARAM_STR)); + + $dateCreated = $this->parseDate($recipe['dateCreated']); + $dateModified = $this->parseDate($recipe['dateModified']); + + $qb->set('date_created', $qb->createNamedParameter($dateCreated, IQueryBuilder::PARAM_DATE)); + $qb->set('date_modified', $qb->createNamedParameter($dateModified, IQueryBuilder::PARAM_DATE)); $qb->setParameter('id', $recipe['id']); $qb->setParameter('uid', $userId); - $qb->execute(); + try { + $qb->execute(); + } catch (\Exception $ex) { + throw $ex; + } } } @@ -536,7 +579,7 @@ public function updateCategoryOfRecipe(int $recipeId, string $categoryName, stri $qb = $this->db->getQueryBuilder(); $qb->update(self::DB_TABLE_CATEGORIES) ->where('recipe_id = :rid', 'user_id = :user'); - $qb->set('name', $qb->expr()->literal($categoryName, IQueryBuilder::PARAM_STR)); + $qb->set('name', $qb->createNamedParameter($categoryName, IQueryBuilder::PARAM_STR)); $qb->setParameter('rid', $recipeId, $this->types->INT()); $qb->setParameter('user', $userId, $this->types->STRING()); $qb->execute(); @@ -608,13 +651,25 @@ public function removeKeywordPairs(array $pairs, string $userId) { foreach ($pairs as $p) { $qb->orWhere( $qb->expr()->andX( - $qb->expr()->eq('user_id', $qb->expr()->literal($userId)), - $qb->expr()->eq('recipe_id', $qb->expr()->literal($p['recipeId'])), - $qb->expr()->eq('name', $qb->expr()->literal($p['name'])) + $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)), + $qb->expr()->eq('recipe_id', $qb->createNamedParameter($p['recipeId'], IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('name', $qb->createNamedParameter($p['name'], IQueryBuilder::PARAM_STR)) ) ); } $qb->execute(); } + + private function parseDate(?string $date) { + if (is_null($date)) { + return null; + } + + try { + return new DateTimeImmutable($date); + } catch (\Exception $ex) { + return new DateTimeImmutable(); + } + } } diff --git a/lib/Helper/Filter/AbstractRecipeFilter.php b/lib/Helper/Filter/AbstractRecipeFilter.php new file mode 100644 index 000000000..7dbd1ab3c --- /dev/null +++ b/lib/Helper/Filter/AbstractRecipeFilter.php @@ -0,0 +1,26 @@ +patternDate = join('|', [ + '^' . self::PATTERN_DATE . '$', + '^' . self::PATTERN_DATE . 'T' . self::PATTERN_TIME . '$' + ]); + } + + public function apply(array &$json, File $recipe): bool { + $ret = false; + + // First ensure the entries are present in general + $this->ensureEntryExists($json, self::DATE_CREATED, $ret); + $this->ensureEntryExists($json, self::DATE_MODIFIED, $ret); + + // Ensure the date formats are valid + $this->checkDateFormat($json, self::DATE_CREATED, $ret); + $this->checkDateFormat($json, self::DATE_MODIFIED, $ret); + + if (is_null($json['dateCreated'])) { + if (is_null($json['dateModified'])) { + // No dates have been set. Fall back to time of file + $json['dateCreated'] = $this->getTimeFromFile($recipe); + $ret = true; + } else { + // Copy over the modification time to the creation time + $json['dateCreated'] = $json['dateModified']; + $ret = true; + } + } + /* + The else case is not considered: + If only the creation time is given, this is a valid recipe (no modifications so far). + If both are given, no problem is present. + */ + + return $ret; + } + + /** + * Undocumented function + * + */ + private function getTimeFromFile(File $file): string { + $timestamp = $file->getCreationTime(); + if ($timestamp === 0) { + $timestamp = $file->getUploadTime(); + } + if ($timestamp === 0) { + $timestamp = $file->getMTime(); + } + + return $this->getDateFromTimestamp($timestamp); + } + + private function getDateFromTimestamp(int $timestamp): string { + $date = new DateTime(); + $date->setTimestamp($timestamp); + + return $date->format(DateTime::ISO8601); + } + + private function ensureEntryExists(array &$json, string $name, bool &$ret) { + if (!array_key_exists($name, $json)) { + $json[$name] = null; + $ret = true; + } + } + + private function checkDateFormat(array &$json, string $name, bool &$ret) { + if ($json[$name] === null) { + return; + } + + // Check for valid date format + if (preg_match('/' . $this->patternDate . '/', $json[$name]) === 1) { + return; + } + + // Last desperate approach: Is it a timestamp? + if (preg_match('/^\d+$/', $json[$name])) { + if ($json[$name] > 0) { + $json[$name] = $this->getDateFromTimestamp($json[$name]); + $ret = true; + return; + } + } + + // We cannot read the format. Removing it from teh recipe + $json[$name] = null; + $ret = true; + return; + } +} diff --git a/lib/Helper/Filter/NormalizeRecipeFileFilter.php b/lib/Helper/Filter/NormalizeRecipeFileFilter.php new file mode 100644 index 000000000..389b5ec39 --- /dev/null +++ b/lib/Helper/Filter/NormalizeRecipeFileFilter.php @@ -0,0 +1,45 @@ +filters = [ + $datesFilter, + ]; + } + + /** + * Normalize a recipe file according to the installed filters + * + * If the recipe needs updating, the file gets overwritten in the storage. + * + * @param array $json The content of the recipe + * @param File $recipeFile The file containing the recipe + * @param bool $updateFiles true, if the file on storage should be updated with the modified version + * @return array The corrected recipe object + */ + public function filter(array $json, File $recipeFile, bool $updateFiles = false): array { + $changed = false; + + foreach ($this->filters as $filter) { + /** @var AbstractRecipeFilter $filter */ + $changed |= $filter->apply($json, $recipeFile); + } + + if ($changed && $updateFiles) { + $recipeFile->putContent(json_encode($json)); + $recipeFile->touch(); + } + + return $json; + } +} diff --git a/lib/Migration/Version000000Date20220703174647.php b/lib/Migration/Version000000Date20220703174647.php new file mode 100644 index 000000000..9cb16ce8e --- /dev/null +++ b/lib/Migration/Version000000Date20220703174647.php @@ -0,0 +1,58 @@ +getTable('cookbook_names'); + + if (! $table->hasColumn('date_created')) { + $table->addColumn('date_created', 'datetime_immutable', [ + 'notnull' => false, + ]); + } + + if (!$table->hasColumn('date_modified')) { + $table->addColumn('date_modified', 'datetime_immutable', [ + 'notnull' => false, + ]); + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Search/Provider.php b/lib/Search/Provider.php index f99eb3718..6f5d3a990 100644 --- a/lib/Search/Provider.php +++ b/lib/Search/Provider.php @@ -3,7 +3,6 @@ namespace OCA\Cookbook\Search; use OCA\Cookbook\AppInfo\Application; -use OCA\Cookbook\Db\RecipeDb; use OCA\Cookbook\Service\RecipeService; use OCP\IL10N; use OCP\IUser; @@ -24,17 +23,16 @@ class Provider implements IProvider { /** @var IURLGenerator */ private $urlGenerator; - /** @var RecipeDb */ - private $recipeDb; - /** @var RecipeService */ private $recipeService; - public function __construct(IL10n $il10n, IURLGenerator $urlGenerator, - RecipeDb $recipeDb, RecipeService $recipeService) { + public function __construct( + IL10n $il10n, + IURLGenerator $urlGenerator, + RecipeService $recipeService + ) { $this->l = $il10n; $this->urlGenerator = $urlGenerator; - $this->recipeDb = $recipeDb; $this->recipeService = $recipeService; } @@ -62,7 +60,7 @@ function (array $recipe) use ($user): SearchResultEntry { $id = $recipe['recipe_id']; $subline = ''; - $category = $this->recipeDb->getCategoryOfRecipe($id, $user->getUID()); + $category = $recipe['category']; if ($category !== null) { // TRANSLATORS Will be shown in search results, listing the recipe category, e.g., 'in Salads' $subline = $this->l->t('in %s', [$category]); diff --git a/lib/Service/DbCacheService.php b/lib/Service/DbCacheService.php index cf386e772..9a6e7d5b1 100644 --- a/lib/Service/DbCacheService.php +++ b/lib/Service/DbCacheService.php @@ -6,6 +6,7 @@ use OCP\Files\File; use OCP\AppFramework\Db\DoesNotExistException; use OCA\Cookbook\Exception\InvalidJSONFileException; +use OCA\Cookbook\Helper\Filter\NormalizeRecipeFileFilter; use OCA\Cookbook\Helper\UserConfigHelper; use OCP\IL10N; @@ -33,6 +34,9 @@ class DbCacheService { */ private $l; + /** @var NormalizeRecipeFileFilter */ + private $normalizeFileFilter; + private $jsonFiles; private $dbReceipeFiles; private $dbKeywords; @@ -43,16 +47,18 @@ class DbCacheService { private $updatedRecipes; public function __construct( - ?string $UserId, - RecipeDb $db, - RecipeService $recipeService, - UserConfigHelper $userConfigHelper, - IL10N $l - ) { + ?string $UserId, + RecipeDb $db, + RecipeService $recipeService, + UserConfigHelper $userConfigHelper, + NormalizeRecipeFileFilter $normalizeRecipeFileFilter, + IL10N $l + ) { $this->userId = $UserId; $this->db = $db; $this->recipeService = $recipeService; $this->userConfigHelper = $userConfigHelper; + $this->normalizeFileFilter = $normalizeRecipeFileFilter; $this->l = $l; } @@ -142,6 +148,8 @@ private function parseJSONFile(File $jsonFile): array { $id = (int) $jsonFile->getParent()->getId(); $json['id'] = $id; + $json = $this->normalizeFileFilter->filter($json, $jsonFile, true); + return $json; } @@ -157,6 +165,8 @@ private function fetchDbRecipeInformations() { $obj = []; $obj['name'] = $row['name']; $obj['id'] = $id; + $obj['dateCreated'] = $row['dateCreated']; + $obj['dateModified'] = $row['dateModified']; $ret[$id] = $obj; } @@ -194,7 +204,7 @@ private function compareReceipeLists() { if (array_key_exists($id, $this->dbReceipeFiles)) { // The file was at least in the database - if (! $this->isDbEntryUpToDate($id)) { + if (!$this->isDbEntryUpToDate($id)) { // An update is needed $this->updatedRecipes[] = $id; } @@ -220,6 +230,13 @@ private function isDbEntryUpToDate($id) { if ($dbEntry['name'] !== $fileEntry['name']) { return false; } + if ($dbEntry['dateCreated'] !== $fileEntry['dateCreated']) { + return false; + } + + if ($dbEntry['dateModified'] !== $fileEntry['dateModified']) { + return false; + } return true; } @@ -273,7 +290,7 @@ private function updateCategories() { * @return bool */ private function hasJSONCategory(array $json): bool { - return ! is_null($this->getJSONCategory($json)); + return !is_null($this->getJSONCategory($json)); } /** @@ -319,7 +336,7 @@ private function updateKeywords() { return trim($v); }, $keywords); $keywords = array_filter($keywords, function ($v) { - return ! empty($v); + return !empty($v); }); $dbKeywords = $this->dbKeywords[$rid]; diff --git a/lib/Service/RecipeService.php b/lib/Service/RecipeService.php index f90def5f3..76d1ef671 100755 --- a/lib/Service/RecipeService.php +++ b/lib/Service/RecipeService.php @@ -942,10 +942,11 @@ public function getAllCategoriesInSearchIndex() { */ private function addDatesToRecipes(array &$recipes) { foreach ($recipes as $i => $recipe) { - // TODO Add data to database instead of reading from files - $r = $this->getRecipeById($recipe['recipe_id']); - $recipes[$i]['dateCreated'] = $r['dateCreated']; - $recipes[$i]['dateModified'] = $r['dateModified']; + if (! array_key_exists('dateCreated', $recipe) || ! array_key_exists('dateModified', $recipe)) { + $r = $this->getRecipeById($recipe['recipe_id']); + $recipes[$i]['dateCreated'] = $r['dateCreated']; + $recipes[$i]['dateModified'] = $r['dateModified']; + } } } diff --git a/tests/Migration/Setup/Migrations/Version000000Date20220703174647Test.php b/tests/Migration/Setup/Migrations/Version000000Date20220703174647Test.php new file mode 100644 index 000000000..0a6f679c1 --- /dev/null +++ b/tests/Migration/Setup/Migrations/Version000000Date20220703174647Test.php @@ -0,0 +1,61 @@ +db->getQueryBuilder(); + $qb->insert('cookbook_names') + ->values([ + 'recipe_id' => ':recipe', + 'user_id' => ':user', + 'name' => ':name', + ]); + $qb->setParameter('name', 'name of the recipe'); + $qb->setParameter('user', 'username'); + $qb->setParameter('recipe', 1234); + + $this->assertEquals(1, $qb->execute()); + + $table = $this->schema->getTable('cookbook_names'); + $this->assertFalse($table->hasColumn('date_created')); + $this->assertFalse($table->hasColumn('date_modified')); + + // Run the migration under test + $this->migrationService->migrate('000000Date20220703174647'); + $this->renewSchema(); + + $table = $this->schema->getTable('cookbook_names'); + $this->assertTrue($table->hasColumn('date_created')); + $this->assertTrue($table->hasColumn('date_modified')); + + $qb = $this->db->getQueryBuilder(); + $qb->select('date_created', 'date_modified')->from('cookbook_names'); + $res = $qb->execute(); + $data = $res->fetchAll(); + + $this->assertEquals(1, count($data)); + $row = $data[0]; + $this->assertEquals(2, count($row)); + $this->assertNull($row['date_created']); + $this->assertNull($row['date_modified']); + + + //$this->assertEquals([null, null], $data[0]); + } + + + protected function getPreviousMigrationName(): string { + return '000000Date20210701093123'; + } +} diff --git a/tests/Unit/Helper/Filter/DB/RecipeDatesFilterTest.php b/tests/Unit/Helper/Filter/DB/RecipeDatesFilterTest.php new file mode 100644 index 000000000..516efd15b --- /dev/null +++ b/tests/Unit/Helper/Filter/DB/RecipeDatesFilterTest.php @@ -0,0 +1,156 @@ +dut = new RecipeDatesFilter(); + } + + public function dpFromJson() { + yield ['2022-07-06T11:08:54', null, '2022-07-06T11:08:54', null, false]; + yield [1657098534, 0, '2022-07-06T09:08:54+0000', null, true]; + yield [1657098534, 1657098540, '2022-07-06T09:08:54+0000', '2022-07-06T09:09:00+0000', true]; + yield [null, 1657098540, '2022-07-06T09:09:00+0000', '2022-07-06T09:09:00+0000', true]; + yield [0, 1657098540, '2022-07-06T09:09:00+0000', '2022-07-06T09:09:00+0000', true]; + } + + /** + * @dataProvider dpFromJson + * @param mixed $created + * @param mixed $modified + * @param mixed $expectedCreation + * @param mixed $expectedModification + * @param mixed $updated + */ + public function testFilterFromJson($created, $modified, $expectedCreation, $expectedModification, $updated) { + $recipe = [ + 'name' => 'my Recipe', + 'dateCreated' => $created, + 'dateModified' => $modified + ]; + $copy = $recipe; + + $file = $this->createStub(File::class); + + $ret = $this->dut->apply($recipe, $file); + + $this->assertEquals($updated, $ret, 'Reporting of modification status'); + $this->assertEquals($expectedCreation, $recipe['dateCreated'], 'Wrong creation date'); + $this->assertEquals($expectedModification, $recipe['dateModified'], 'Wrong modification date'); + + unset($recipe['dateCreated']); + unset($recipe['dateModified']); + unset($copy['dateCreated']); + unset($copy['dateModified']); + + $this->assertEquals($copy, $recipe, 'Other entries must not change.'); + } + + public function dpDateFormats() { + return [ + ['2022-07-05'], + ['2022-07-05T15:30:00'], + ['2022-07-05T15:30:00.123'], + ['2022-07-05T15:30:00z'], + ['2022-07-05T15:30:00Z'], + ['2022-07-05T15:30:00 UTC'], + ['2022-07-05T15:30:00+0100'], + ['2022-07-05T15:30:00-0100'], + ['2022-07-05T15:30:00+01:00'], + ['2022-07-05T15:30:00-01:00'], + ['2022-07-05T15:30:00+01'], + ['2022-07-05T15:30:00-01'], + ['2022-07-05T15:30:00.123+01:00'], + ]; + } + + /** + * @dataProvider dpDateFormats + * @param mixed $date + */ + public function testDateFormats($date) { + $recipe = [ + 'name' => 'my Recipe', + 'dateCreated' => $date, + 'dateModified' => $date + ]; + $copy = $recipe; + + $file = $this->createStub(File::class); + + $ret = $this->dut->apply($recipe, $file); + + $this->assertFalse($ret, 'Reporting of modification status'); + $this->assertEquals($date, $recipe['dateCreated'], 'Wrong creation date'); + $this->assertEquals($date, $recipe['dateModified'], 'Wrong modification date'); + + unset($recipe['dateCreated']); + unset($recipe['dateModified']); + unset($copy['dateCreated']); + unset($copy['dateModified']); + + $this->assertEquals($copy, $recipe, 'Other entries must not change.'); + } + + public function dpFromFile() { + yield ['2022-07-06T09:08:54+0000', false, false, 1657098534, 1657098535, 1657098536]; + yield ['2022-07-06T09:08:55+0000', false, false, 0, 1657098535, 1657098536]; + yield ['2022-07-06T09:08:56+0000', false, false, 0, 0, 1657098536]; + yield ['2022-07-06T09:08:54+0000', true, true, 1657098534, 1657098535, 1657098536]; + } + + /** + * @dataProvider dpFromFile + * @param mixed $creation + * @param mixed $creationPresent + * @param mixed $modificationPresent + * @param mixed $creationTime + * @param mixed $uploadTime + * @param mixed $mTime + */ + public function testFilterFromFile($creation, $creationPresent, $modificationPresent, $creationTime, $uploadTime, $mTime) { + $recipe = [ + 'name' => 'my Recipe', + ]; + if ($creationPresent) { + $recipe['dateCreated'] = null; + } + if ($modificationPresent) { + $recipe['dateModified'] = null; + } + + $copy = $recipe; + + /** @var Stub|File */ + $file = $this->createStub(File::class); + $file->method('getCreationTime')->willReturn($creationTime); + $file->method('getUploadTime')->willReturn($uploadTime); + $file->method('getMTime')->willReturn($mTime); + + $ret = $this->dut->apply($recipe, $file); + + $this->assertTrue($ret, 'Reporting of modification status'); + + $this->assertEquals($creation, $recipe['dateCreated'], 'Wrong creation date'); + $this->assertNull($recipe['dateModified'], 'Wrong modification date'); + + unset($recipe['dateCreated']); + unset($recipe['dateModified']); + unset($copy['dateCreated']); + unset($copy['dateModified']); + + $this->assertEquals($copy, $recipe, 'Other entries must not change.'); + } +} diff --git a/tests/Unit/Helper/Filter/NormalizeRecipeFileFilterTest.php b/tests/Unit/Helper/Filter/NormalizeRecipeFileFilterTest.php new file mode 100644 index 000000000..d7143434d --- /dev/null +++ b/tests/Unit/Helper/Filter/NormalizeRecipeFileFilterTest.php @@ -0,0 +1,61 @@ +datesFilter = $this->createMock(RecipeDatesFilter::class); + $this->dut = new NormalizeRecipeFileFilter($this->datesFilter); + } + + public function dp() { + yield [false, false, false]; + yield [false, true, false]; + yield [false, false, false]; + yield [true, true, true]; + } + + /** @dataProvider dp */ + public function testFilter($updateRequested, $changedDates, $shouldWrite) { + $recipe = ['name' => 'The recipe']; + + $recipeA = $recipe; + $recipeA['version'] = 'from RecipeDatesFilter'; + + /** @var MockObject|File $recipeFile */ + $recipeFile = $this->createMock(File::class); + + if ($shouldWrite) { + $recipeFile->expects($this->once())->method('putContent')->with(json_encode($recipeA)); + $recipeFile->expects($this->once())->method('touch'); + } else { + $recipeFile->expects($this->never())->method('putContent'); + $recipeFile->expects($this->never())->method('touch'); + } + + $this->datesFilter->method('apply')->with($recipe, $recipeFile) + ->willReturnCallback(function (&$json, $file) use ($changedDates, $recipeA) { + $json = $recipeA; + return $changedDates; + }); + + $ret = $this->dut->filter($recipe, $recipeFile, $updateRequested); + + $this->assertEquals($recipeA, $ret); + } +}