From f0f07ab3f0532ec549f6c16c867e1d4464daf9a4 Mon Sep 17 00:00:00 2001 From: Jan Britz Date: Thu, 1 Aug 2024 17:10:19 +0200 Subject: [PATCH] feat: adjust to qppe changes --- classes/api/api.php | 23 +-- classes/external/load_packages.php | 26 ++- classes/package/package_base.php | 24 +-- classes/package/package_info.php | 179 ++++++++++++++++++ classes/package/package_raw.php | 175 +---------------- classes/package/package_version.php | 1 + .../package/package_version_specific_info.php | 48 +++++ classes/package/package_versions_info.php | 147 ++++++++++++++ classes/question_service.php | 26 +-- db/install.xml | 2 + tests/data_provider.php | 35 ++-- tests/external/favourite_package_test.php | 24 +-- tests/external/get_tags_test.php | 4 +- tests/external/search_packages_test.php | 119 +++--------- tests/package/package_raw_test.php | 135 ------------- tests/package/package_test.php | 17 +- tests/package/package_version_test.php | 17 +- tests/package/package_versions_info_test.php | 170 +++++++++++++++++ tests/question_service_test.php | 58 +++--- version.php | 2 +- 20 files changed, 685 insertions(+), 547 deletions(-) create mode 100644 classes/package/package_info.php create mode 100644 classes/package/package_version_specific_info.php create mode 100644 classes/package/package_versions_info.php create mode 100644 tests/package/package_versions_info_test.php diff --git a/classes/api/api.php b/classes/api/api.php index 4cfb3824..1b47ef57 100644 --- a/classes/api/api.php +++ b/classes/api/api.php @@ -19,6 +19,7 @@ use moodle_exception; use qtype_questionpy\array_converter\array_converter; use qtype_questionpy\package\package_raw; +use qtype_questionpy\package\package_versions_info; use stored_file; use TypeError; @@ -33,7 +34,7 @@ class api { /** * Retrieves QuestionPy packages from the application server. * - * @return package_raw[] + * @return package_versions_info[] * @throws moodle_exception */ public function get_packages(): array { @@ -45,7 +46,7 @@ public function get_packages(): array { $result = []; foreach ($packages as $package) { try { - $result[] = array_converter::from_array(package_raw::class, $package); + $result[] = array_converter::from_array(package_versions_info::class, $package); } catch (TypeError $e) { // TODO: decide what to do with faulty package. debugging($e->getMessage()); @@ -55,24 +56,6 @@ public function get_packages(): array { return $result; } - /** - * Retrieves the package with the given hash, returns null if not found. - * - * @param string $hash the hash of the package to get - * @return ?package_raw the package with the given hash or null if not found - * @throws moodle_exception - */ - public function get_package(string $hash): ?package_raw { - $connector = connector::default(); - $response = $connector->get("/packages/$hash"); - - if ($response->code === 404) { - return null; - } - $response->assert_2xx(); - return array_converter::from_array(package_raw::class, $response->get_data()); - } - /** * Returns the {@see package_api} of a specific package. * diff --git a/classes/external/load_packages.php b/classes/external/load_packages.php index ae81fcd9..f7757b94 100644 --- a/classes/external/load_packages.php +++ b/classes/external/load_packages.php @@ -27,6 +27,7 @@ use external_value; use moodle_exception; use qtype_questionpy\api\api; +use qtype_questionpy\package\package; use qtype_questionpy\package\package_version; /** @@ -60,17 +61,28 @@ public static function execute(): array { $transaction = $DB->start_delegated_transaction(); - // Remove every package version. - $versions = package_version::get_many(); - foreach ($versions as $version) { - $version->delete(); - } - // Load and store packages from the application server. $api = new api(); $packages = $api->get_packages(); + $incomingpackageids = []; foreach ($packages as $package) { - $package->store(); + [$incomingpackageids[], ] = $package->upsert(); + } + + // Remove old packages. + if (empty($incomingpackageids)) { + // There are no incoming packages -> remove every package. + $packages = package::get_records(); + foreach ($packages as $package) { + $package->delete(); + } + } else { + [$sql, $params] = $DB->get_in_or_equal($incomingpackageids, SQL_PARAMS_NAMED, 'packageid', false); + $removedpackageids = $DB->get_fieldset_select('qtype_questionpy_package', 'id', "id $sql", $params); + foreach ($removedpackageids as $id) { + [$package] = package::get_records(['id' => $id]); + $package->delete(); + } } $transaction->allow_commit(); diff --git a/classes/package/package_base.php b/classes/package/package_base.php index e672b445..2a5c0632 100644 --- a/classes/package/package_base.php +++ b/classes/package/package_base.php @@ -30,57 +30,57 @@ class package_base { /** * @var string package shortname */ - protected string $shortname; + public readonly string $shortname; /** * @var string package namespace */ - protected string $namespace; + public readonly string $namespace; /** * @var array package name */ - protected array $name; + public readonly array $name; /** * @var string package type */ - protected string $type; + public readonly string $type; /** * @var string|null package author */ - protected ?string $author; + public readonly ?string $author; /** * @var string|null package url */ - protected ?string $url; + public readonly ?string $url; /** * @var array|null package languages */ - protected ?array $languages; + public readonly ?array $languages; /** * @var array|null package description */ - protected ?array $description; + public readonly ?array $description; /** * @var string|null package icon */ - protected ?string $icon; + public readonly ?string $icon; /** * @var string|null package license */ - protected ?string $license; + public readonly ?string $license; /** * @var array|null package tags */ - protected ?array $tags; + public readonly ?array $tags; /** * Constructs package class. @@ -118,7 +118,7 @@ public function __construct(string $shortname, string $namespace, array $name, s /** * Creates a localized array representation of the package. * - * @param array|null $languages + * @param array $languages * @return array array representation of the package * @throws moodle_exception */ diff --git a/classes/package/package_info.php b/classes/package/package_info.php new file mode 100644 index 00000000..03adbe64 --- /dev/null +++ b/classes/package/package_info.php @@ -0,0 +1,179 @@ +. + +namespace qtype_questionpy\package; + +defined('MOODLE_INTERNAL') || die; + +use moodle_exception; +use qtype_questionpy\array_converter\array_converter; +use qtype_questionpy\array_converter\converter_config; + +/** + * Represents an overview of available QuestionPy packages on the application server. + * + * @package qtype_questionpy + * @copyright 2024 Jan Britz, TU Berlin, innoCampus - www.questionpy.org + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class package_info extends package_base { + /** + * Checks whether this package exists or not. + * + * @return bool|int either false or the DB id + * @throws moodle_exception + */ + public function exists(): bool|int { + global $DB; + return $DB->get_field('qtype_questionpy_package', 'id', [ + 'shortname' => $this->shortname, + 'namespace' => $this->namespace, + ]); + } + + /** + * Persists a package tag in the database. + * + * @param string $tag + * @return int + * @throws moodle_exception + */ + private function store_tag(string $tag): int { + global $DB; + + $record = ['tag' => strtolower($tag)]; + + $id = $DB->get_field('qtype_questionpy_tag', 'id', $record); + if ($id === false) { + return $DB->insert_record('qtype_questionpy_tag', $record); + } + return $id; + } + + /** + * Inserts new package data. + * + * @param int $timestamp + * @return int + * @throws moodle_exception + */ + public function insert(int $timestamp): int { + global $DB; + $transaction = $DB->start_delegated_transaction(); + $id = $DB->insert_record('qtype_questionpy_package', [ + 'shortname' => $this->shortname, + 'namespace' => $this->namespace, + 'type' => $this->type, + 'author' => $this->author, + 'url' => $this->url, + 'icon' => $this->icon, + 'license' => $this->license, + 'timemodified' => $timestamp, + 'timecreated' => $timestamp, + ]); + + if ($this->languages) { + // For each language store the localized package data as a separate record. + $languagedata = []; + foreach ($this->languages as $language) { + $languagedata[] = [ + 'packageid' => $id, + 'language' => $language, + 'name' => $this->get_localized_name([$language]), + 'description' => $this->get_localized_description([$language]), + ]; + } + $DB->insert_records('qtype_questionpy_language', $languagedata); + } + + if ($this->tags) { + // Store each tag with the package id in the tag table. + $tagsdata = []; + foreach ($this->tags as $tag) { + $tagsdata[] = [ + 'packageid' => $id, + 'tagid' => $this->store_tag($tag), + ]; + } + $DB->insert_records('qtype_questionpy_pkgtag', $tagsdata); + } + + $transaction->allow_commit(); + return $id; + } + + /** + * Updates existing package info. + * + * @param int $id + * @param int $timestamp + * @throws moodle_exception + */ + public function update(int $id, int $timestamp): void { + global $DB; + $transaction = $DB->start_delegated_transaction(); + $DB->update_record('qtype_questionpy_package', [ + 'id' => $id, + 'type' => $this->type, + 'author' => $this->author, + 'url' => $this->url, + 'icon' => $this->icon, + 'license' => $this->license, + 'timemodified' => $timestamp, + ]); + + // We remove every language and tag entry and insert the new ones for simplicity. + $DB->delete_records('qtype_questionpy_language', ['packageid' => $id]); + foreach ($this->languages as $language) { + $DB->insert_record('qtype_questionpy_language', [ + 'packageid' => $id, + 'language' => $language, + 'name' => $this->get_localized_name([$language]), + 'description' => $this->get_localized_description([$language]), + ]); + } + + $DB->delete_records('qtype_questionpy_pkgtag', ['packageid' => $id]); + // Store each tag with the package id in the tag table. + $tagsdata = []; + foreach ($this->tags as $tag) { + $tagsdata[] = [ + 'packageid' => $id, + 'tagid' => $this->store_tag($tag), + ]; + } + $DB->insert_records('qtype_questionpy_pkgtag', $tagsdata); + $DB->execute(" + DELETE + FROM {qtype_questionpy_tag} + WHERE id NOT IN ( + SELECT tagid + FROM {qtype_questionpy_pkgtag} + ) + "); + + $transaction->allow_commit(); + } +} + +array_converter::configure(package_info::class, function (converter_config $config) { + $config + ->rename("hash", "package_hash") + ->rename("shortname", "short_name") + // The DB rows are also read using array_converter, but their columns are named differently to the json fields. + ->alias("hash", "hash") + ->alias("shortname", "shortname"); +}); diff --git a/classes/package/package_raw.php b/classes/package/package_raw.php index 475863ad..ac1de9d4 100644 --- a/classes/package/package_raw.php +++ b/classes/package/package_raw.php @@ -25,8 +25,7 @@ /** * Represents a QuestionPy package from a server. * - * It contains metadata about a package version and its package. The raw package can be stored in the database and the - * data will then be accessible through {@see package} and {@see package_version}. + * It contains metadata about a package version and its package. * * @package qtype_questionpy * @copyright 2023 Jan Britz, TU Berlin, innoCampus - www.questionpy.org @@ -42,178 +41,6 @@ class package_raw extends package_base { * @var string package version */ public string $version; - - /** - * Constructs package class. - * - * @param string $hash - * @param string $shortname - * @param string $namespace - * @param array $name - * @param string $version - * @param string $type - * @param string|null $author - * @param string|null $url - * @param array|null $languages - * @param array|null $description - * @param string|null $icon - * @param string|null $license - * @param array|null $tags - */ - public function __construct(string $hash, string $shortname, string $namespace, array $name, string $version, - string $type, ?string $author = null, ?string $url = null, ?array $languages = null, - ?array $description = null, ?string $icon = null, ?string $license = null, - ?array $tags = null) { - $this->hash = $hash; - $this->version = $version; - parent::__construct( - $shortname, - $namespace, - $name, - $type, - $author, - $url, - $languages, - $description, - $icon, - $license, - $tags - ); - } - - /** - * Persists a package version in the database. - * - * @param int $packageid - * @param int $timecreated - * @return int - * @throws moodle_exception - */ - private function store_pkgversion(int $packageid, int $timecreated): int { - global $DB; - - return $DB->insert_record('qtype_questionpy_pkgversion', [ - 'packageid' => $packageid, - 'hash' => $this->hash, - 'version' => $this->version, - 'timecreated' => $timecreated, - ]); - } - - /** - * Persists a package tag in the database. - * - * @param string $tag - * @return int - * @throws moodle_exception - */ - private function store_tag(string $tag): int { - global $DB; - - $record = ['tag' => $tag]; - - $id = $DB->get_field('qtype_questionpy_tag', 'id', $record); - if ($id === false) { - // TODO: store them in upper- or lowercase? - return $DB->insert_record('qtype_questionpy_tag', $record); - } - return $id; - } - - /** - * Persists a package in the database. - * - * @param int $timestamp - * @return int - * @throws moodle_exception - */ - private function store_package(int $timestamp): int { - global $DB; - - $packageid = $DB->insert_record('qtype_questionpy_package', [ - 'shortname' => $this->shortname, - 'namespace' => $this->namespace, - 'type' => $this->type, - 'author' => $this->author, - 'url' => $this->url, - 'icon' => $this->icon, - 'license' => $this->license, - 'timemodified' => $timestamp, - 'timecreated' => $timestamp, - ]); - - if ($this->languages) { - // For each language store the localized package data as a separate record. - $languagedata = []; - foreach ($this->languages as $language) { - $languagedata[] = [ - 'packageid' => $packageid, - 'language' => $language, - 'name' => $this->get_localized_name([$language]), - 'description' => $this->get_localized_description([$language]), - ]; - } - $DB->insert_records('qtype_questionpy_language', $languagedata); - } - - if ($this->tags) { - // Store each tag with the package id in the tag table. - $tagsdata = []; - foreach ($this->tags as $tag) { - $tagsdata[] = [ - 'packageid' => $packageid, - 'tagid' => $this->store_tag($tag), - ]; - } - $DB->insert_records('qtype_questionpy_pkgtag', $tagsdata); - } - return $packageid; - } - - /** - * Persist this package in the database. - * - * Localized data is stored in qtype_questionpy_language. - * Tags are mapped packageid->tag in the table qtype_questionpy_tags. - * - * @return int the ID of the inserted record in the DB - * @throws moodle_exception - */ - public function store(): int { - global $DB; - - $timestamp = time(); - - $packageid = $DB->get_field('qtype_questionpy_package', 'id', [ - 'shortname' => $this->shortname, - 'namespace' => $this->namespace, - ]); - - if (!$packageid) { - // Package does not exist -> add it. - $transaction = $DB->start_delegated_transaction(); - $packageid = $this->store_package($timestamp); - } else { - // Package does already exist - check if the version also exists. - $pkgversion = package_version::get_by_package_and_version($packageid, $this->version); - if ($pkgversion) { - // Package version does already exist. - if ($pkgversion->hash !== $this->hash) { - // Version does not match existing hash. - // TODO: decide what to do. - throw new moodle_exception('same_version_different_hash_error', 'qtype_questionpy'); - } - return $pkgversion->id; - } - // Package version does not exist. - $transaction = $DB->start_delegated_transaction(); - } - // Create package version. - $pkgversionid = $this->store_pkgversion($packageid, $timestamp); - - $transaction->allow_commit(); - return $pkgversionid; - } } array_converter::configure(package_raw::class, function (converter_config $config) { diff --git a/classes/package/package_version.php b/classes/package/package_version.php index 020543d1..38727e59 100644 --- a/classes/package/package_version.php +++ b/classes/package/package_version.php @@ -65,6 +65,7 @@ public static function sql_get(string $where = '', array $params = []): array { SELECT id, packageid, hash, version FROM {qtype_questionpy_pkgversion} $where + ORDER BY versionorder "; return [$sql, $params]; } diff --git a/classes/package/package_version_specific_info.php b/classes/package/package_version_specific_info.php new file mode 100644 index 00000000..ff422c0f --- /dev/null +++ b/classes/package/package_version_specific_info.php @@ -0,0 +1,48 @@ +. + +namespace qtype_questionpy\package; + +defined('MOODLE_INTERNAL') || die; + +use qtype_questionpy\array_converter\array_converter; +use qtype_questionpy\array_converter\converter_config; + +/** + * Represents a package version of an available QuestionPy package on the application server. + * + * @package qtype_questionpy + * @copyright 2024 Jan Britz, TU Berlin, innoCampus - www.questionpy.org + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class package_version_specific_info { + /** + * @var string $hash + */ + public readonly string $hash; + + /** + * @var string $version + */ + public readonly string $version; +} + +array_converter::configure(package_version_specific_info::class, function (converter_config $config) { + $config + ->rename("hash", "package_hash") + // The DB rows are also read using array_converter, but their columns are named differently to the json fields. + ->alias("hash", "hash"); +}); diff --git a/classes/package/package_versions_info.php b/classes/package/package_versions_info.php new file mode 100644 index 00000000..efa140f5 --- /dev/null +++ b/classes/package/package_versions_info.php @@ -0,0 +1,147 @@ +. + +namespace qtype_questionpy\package; + +defined('MOODLE_INTERNAL') || die; + +use moodle_exception; +use qtype_questionpy\array_converter\array_converter; +use qtype_questionpy\array_converter\converter_config; + +/** + * Represents a QuestionPy package and its versions on the application server. + * + * @package qtype_questionpy + * @copyright 2024 Jan Britz, TU Berlin, innoCampus - www.questionpy.org + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class package_versions_info { + /** + * @var package_info $manifest + */ + public package_info $manifest; + + /** + * @var package_version_specific_info[] $versions + */ + public array $versions; + + /** + * Upserts the package info and versions in the database. + * + * Returns an array where the first element is the package id and the second element is an array of version ids sorted by their + * version order. + * + * @param int|null $timestamp + * @return array + * @throws moodle_exception + */ + public function upsert(?int $timestamp = null): array { + global $DB; + + $timestamp ??= time(); + + $packageid = $this->manifest->exists(); + if ($packageid === false) { + $packageid = $this->manifest->insert($timestamp); + $versionids = $this->insert($packageid, $timestamp); + } else { + // Get the latest package version stored in the DB and check if we need to update the package info. + $latestexisting = $DB->get_field('qtype_questionpy_pkgversion', 'hash', ['packageid' => $packageid, + 'versionorder' => 0]); + if ($this->versions[0]->hash !== $latestexisting) { + $this->manifest->update($packageid, $timestamp); + } + $versionids = $this->update($packageid, $timestamp); + } + + return [$packageid, $versionids]; + } + + /** + * Inserts the package versions of a new package. + * + * @param int $packageid + * @param int $timestamp + * @return array + * @throws moodle_exception + */ + private function insert(int $packageid, int $timestamp): array { + global $DB; + + $versionids = []; + foreach ($this->versions as $index => $version) { + $versionids[] = $DB->insert_record('qtype_questionpy_pkgversion', ['packageid' => $packageid, + 'version' => $version->version, 'hash' => $version->hash, 'versionorder' => $index, 'timemodified' => $timestamp, + 'timecreated' => $timestamp]); + } + return $versionids; + } + + /** + * Updates the package versions of an existing package. + * + * @param int $packageid + * @param int $timestamp + * @return array + * @throws moodle_exception + */ + private function update(int $packageid, int $timestamp): array { + global $DB; + + $existingrecords = $DB->get_records('qtype_questionpy_pkgversion', ['packageid' => $packageid], 'versionorder', 'hash, id'); + + $existing = array_column($existingrecords, 'hash'); + $incoming = array_column($this->versions, 'hash'); + + if ($existing === $incoming) { + // There are no new or missing package versions. + return array_column($existingrecords, 'id'); + } + + // Delete previously existing package versions. + $old = array_diff($existing, $incoming); + if (!empty($old)) { + [$sql, $params] = $DB->get_in_or_equal($old, SQL_PARAMS_NAMED, 'hashes'); + $params['packageid'] = $packageid; + $DB->delete_records_select('qtype_questionpy_pkgversion', "packageid = :packageid AND hash $sql", $params); + } + + // Add new or update previously existing package versions. + $versionids = []; + $new = array_flip(array_diff($incoming, $existing)); + foreach ($this->versions as $index => $version) { + if (isset($new[$version->hash])) { + $versionids[] = $DB->insert_record('qtype_questionpy_pkgversion', ['packageid' => $packageid, + 'version' => $version->version, 'hash' => $version->hash, 'versionorder' => $index, + 'timemodified' => $timestamp, 'timecreated' => $timestamp]); + } else { + // The get_records(...) returns an array indexed by the first field which we set to be the hash of the version. + $versionid = $existingrecords[$version->hash]->id; + $versionids[] = $versionid; + $DB->update_record('qtype_questionpy_pkgversion', ['id' => $versionid, 'versionorder' => $index, + 'timemodified' => $timestamp]); + } + } + return $versionids; + } +} + +array_converter::configure(package_versions_info::class, function (converter_config $config) { + $config + ->array_elements('versions', package_version_specific_info::class); +}); diff --git a/classes/question_service.php b/classes/question_service.php index 9ce6665e..d2ba4682 100644 --- a/classes/question_service.php +++ b/classes/question_service.php @@ -131,7 +131,7 @@ public function upsert_question(object $question): void { $file = $this->packagefileservice->get_draft_file($question->qpy_package_file); } } else { - $pkgversionid = $this->get_package($question->qpy_package_hash); + $pkgversionid = package_version::get_by_hash($question->qpy_package_hash)->id ?? null; if (!$pkgversionid) { throw new moodle_exception( 'package_not_found', @@ -212,28 +212,4 @@ public static function delete_question(int $questionid) { $DB->delete_records(self::QUESTION_TABLE, ['questionid' => $questionid]); // TODO: Also delete packages when they are no longer used by any question. } - - /** - * Get the package id with the given hash from the DB or the QuestionPy server API. - * - * If the package isn't found in the DB, then it is retrieved from the API and stored in the DB. If it isn't found - * by the API either, `null` is returned. - * - * @param string $hash hash of the package to look for - * @return int|null - * @throws dml_exception - * @throws moodle_exception - */ - private function get_package(string $hash): ?int { - $result = package_version::get_by_hash($hash)->id ?? null; - if ($result) { - return $result; - } - - $package = $this->api->get_package($hash); - if (!$package) { - return null; - } - return $package->store(); - } } diff --git a/db/install.xml b/db/install.xml index 583d51c6..eafabe6f 100644 --- a/db/install.xml +++ b/db/install.xml @@ -62,6 +62,8 @@ + + diff --git a/tests/data_provider.php b/tests/data_provider.php index 1a07db84..51ab9b9e 100644 --- a/tests/data_provider.php +++ b/tests/data_provider.php @@ -42,30 +42,30 @@ use qtype_questionpy\form\elements\static_text_element; use qtype_questionpy\form\elements\text_area_element; use qtype_questionpy\form\elements\text_input_element; -use qtype_questionpy\package\package_raw; +use qtype_questionpy\package\package_versions_info; /** - * Returns a raw package object which can be modified by an array of attributes. + * Returns a {@see package_versions_info}-object which can be modified. * * The languages array gets generated when it is not set inside {@see $attributes}. * - * @param array $attributes - * @return package_raw + * @param array|null $packageinfo + * @param array|null $versions + * @return package_versions_info * @throws moodle_exception */ -function package_provider(array $attributes = []): package_raw { - $data = array_merge([ +function package_versions_info_provider(?array $packageinfo = null, ?array $versions = null): package_versions_info { + $packageinfo = array_merge([ 'short_name' => 'my_short_name', 'namespace' => 'my_namespace', 'name' => [ 'en' => 'en: My Name', 'de' => 'de: My Name', ], - 'version' => '0.1.0', 'type' => 'questiontype', 'author' => 'John Doe', - 'url' => 'http://www.example.com/', + 'url' => 'https://www.example.com/', 'languages' => ['en', 'de'], 'description' => [ 'en' => 'en: Lorem ipsum dolor sit amet.', @@ -74,22 +74,27 @@ function package_provider(array $attributes = []): package_raw { 'icon' => 'https://placehold.jp/40e47e/598311/150x150.png', 'license' => 'MIT', 'tags' => ['my_tag_0', 'my_tag_1', 'my_tag_2'], - ], $attributes); + ], $packageinfo ?? []); // Create 'languages' array based on provided 'name' and 'description' translations if none is provided. if ((isset($attributes['name']) || isset($attributes['description'])) && !isset($attributes['languages'])) { foreach (['name', 'description'] as $field) { - $data['languages'] = array_merge($data['languages'], array_keys($data[$field])); + $packageinfo['languages'] = array_merge($packageinfo['languages'], array_keys($packageinfo[$field])); } - $data['languages'] = array_values(array_unique($data['languages'])); + $packageinfo['languages'] = array_values(array_unique($packageinfo['languages'])); } - // Calculate package hash if none is provided. - if (!isset($attributes['package_hash'])) { - $data['package_hash'] = hash('sha256', $data['short_name'] . $data['namespace'] . $data['version']); + $versions ??= [['version' => '0.1.0']]; + + foreach ($versions as &$version) { + // Calculate package hash if none is provided. + if (!isset($version['hash'])) { + $version['hash'] = hash('sha256', $packageinfo['short_name'] . $packageinfo['namespace'] . $version['version']); + } } - return array_converter::from_array(package_raw::class, $data); + + return array_converter::from_array(package_versions_info::class, ['manifest' => $packageinfo, 'versions' => $versions]); } /** diff --git a/tests/external/favourite_package_test.php b/tests/external/favourite_package_test.php index 6001d553..15dd1cd3 100644 --- a/tests/external/favourite_package_test.php +++ b/tests/external/favourite_package_test.php @@ -26,7 +26,7 @@ use context_user; use external_api; use moodle_exception; -use function qtype_questionpy\package_provider; +use function qtype_questionpy\package_versions_info_provider; defined('MOODLE_INTERNAL') || die(); @@ -54,18 +54,6 @@ public function setUp(): void { $this->setGuestUser(); } - /** - * Returns the package id of the given package version id. - * - * @param int $pkgversionid - * @return int - * @throws moodle_exception - */ - private static function get_id(int $pkgversionid): int { - global $DB; - return $DB->get_field('qtype_questionpy_pkgversion', 'packageid', ['id' => $pkgversionid], MUST_EXIST); - } - /** * Asserts that the given packages are retrievable via the user favourite service. * @@ -115,7 +103,7 @@ public function test_favourite_with_not_existing_package_id_does_not_work(): voi */ public function test_favourite_works_with_user_package(): void { global $USER; - $packageid = self::get_id(package_provider()->store()); + [$packageid, ] = package_versions_info_provider()->upsert(); $res = favourite_package::execute($packageid, true); $res = external_api::clean_returnvalue(favourite_package::execute_returns(), $res); self::assertTrue($res); @@ -130,7 +118,7 @@ public function test_favourite_works_with_user_package(): void { */ public function test_favourite_works_with_server_package(): void { global $USER; - $packageid = self::get_id(package_provider()->store()); + [$packageid, ] = package_versions_info_provider()->upsert(); $res = favourite_package::execute($packageid, true); $res = external_api::clean_returnvalue(favourite_package::execute_returns(), $res); self::assertTrue($res); @@ -150,7 +138,7 @@ public function test_favourite_works_with_packages_uploaded_by_other_user(): voi // Upload a package as user one. $this->setUser($user1); - $packageid = self::get_id(package_provider()->store()); + [$packageid, ] = package_versions_info_provider()->upsert(); // Favourite the package as user two. $this->setUser($user2); @@ -169,7 +157,7 @@ public function test_favourite_works_with_packages_uploaded_by_other_user(): voi */ public function test_favourite_works_when_marking_same_package_multiple_times_as_favourite(): void { global $USER; - $packageid = self::get_id(package_provider()->store()); + [$packageid, ] = package_versions_info_provider()->upsert(); for ($i = 0; $i < 3; $i++) { $res = favourite_package::execute($packageid, true); $res = external_api::clean_returnvalue(favourite_package::execute_returns(), $res); @@ -202,7 +190,7 @@ public function test_unfavourite_with_existing_package_id_does_work(): void { global $USER; // Create a package and favourite it via the user favourite service. - $packageid = self::get_id(package_provider()->store()); + [$packageid, ] = package_versions_info_provider()->upsert(); $context = context_user::instance($USER->id); $ufservice = \core_favourites\service_factory::get_service_for_user_context($context); $ufservice->create_favourite('qtype_questionpy', 'package', $packageid, $context); diff --git a/tests/external/get_tags_test.php b/tests/external/get_tags_test.php index 9bc0bda2..b20b126e 100644 --- a/tests/external/get_tags_test.php +++ b/tests/external/get_tags_test.php @@ -25,7 +25,7 @@ use external_api; use moodle_exception; -use function qtype_questionpy\package_provider; +use function qtype_questionpy\package_versions_info_provider; defined('MOODLE_INTERNAL') || die(); @@ -132,7 +132,7 @@ public function test_get_tags(string $query, array $packagetags, array $expected $this->setGuestUser(); foreach ($packagetags as $i => $tags) { - package_provider(['namespace' => "ns$i", 'tags' => $tags])->store(); + package_versions_info_provider(['namespace' => "ns$i", 'tags' => $tags])->upsert(); } $tags = get_tags::execute($query); $tags = external_api::clean_returnvalue(get_tags::execute_returns(), $tags); diff --git a/tests/external/search_packages_test.php b/tests/external/search_packages_test.php index e5061d6e..5c463c0e 100644 --- a/tests/external/search_packages_test.php +++ b/tests/external/search_packages_test.php @@ -28,7 +28,7 @@ use core_favourites\local\service\user_favourite_service; use external_api; use moodle_exception; -use function qtype_questionpy\package_provider; +use function qtype_questionpy\package_versions_info_provider; defined('MOODLE_INTERNAL') || die(); @@ -197,57 +197,6 @@ public function test_search_without_available_packages(): void { ], $res); } - /** - * Acts as a provider for {@see test_user_and_server_versions_get_returned}. - * - * @return array[] - */ - public static function as_user_provider(): array { - return [ - [true], - [false], - ]; - } - - /** - * Tests that packages uploaded by the user and packages retrieved by the server are returned by the service. - * - * @param bool $asuser Whether the packages were uploaded as user or not. - * @covers \qtype_questionpy\external\search_packages::execute - * @dataProvider as_user_provider - * @throws moodle_exception - */ - public function test_user_and_server_versions_get_returned(bool $asuser): void { - // Create packages and their versions. - $totalpackages = 2; - $totalversions = 3; - - $versions = []; - for ($i = 0; $i < $totalpackages; $i++) { - $namespace = "n$i"; - for ($j = 0; $j < $totalversions; $j++) { - $versionid = package_provider(['namespace' => $namespace, 'version' => "0.$j.0"])->store($asuser); - $versions[$namespace][] = $versionid; - } - } - - // Execute service. - $res = search_packages::execute('', [], 'all', 'alpha', 'asc', $totalpackages, 0, null); - $res = external_api::clean_returnvalue(search_packages::execute_returns(), $res); - - // Check if every version in DB is returned under the correct package. - $this->assertCount($totalpackages, $res['packages']); - foreach ($res['packages'] as $package) { - $this->assertCount($totalversions, $package['versions']); - foreach ($package['versions'] as $version) { - $this->assertContains($version['id'], $versions[$package['namespace']]); - } - } - - // The amount of package versions should not change the package count. - $this->assert_count_and_total($res, $totalpackages, $totalpackages); - } - /** * Acts as a provider for {@see test_query}. * @@ -386,7 +335,7 @@ public function test_query(string $query, ?string $language, array $packages, ar // Store every package in the database. foreach ($packages as $namespace => [$names, $description]) { - package_provider(['namespace' => $namespace, 'name' => $names, 'description' => $description])->store(); + package_versions_info_provider(['namespace' => $namespace, 'name' => $names, 'description' => $description])->upsert(); } // Set current language if provided. @@ -442,7 +391,7 @@ public static function names_provider(): array { public function test_alphabetical_sort(array $names): void { $totalpackages = count($names); foreach ($names as $name) { - package_provider(['namespace' => "ns$name", 'name' => ['en' => $name]])->store(); + package_versions_info_provider(['namespace' => "ns$name", 'name' => ['en' => $name]])->upsert(); } // Sort the array of names so that we can use it as a reference. @@ -497,7 +446,7 @@ public function test_date_sort(): void { $namespaces = []; for ($i = 0; $i < $totalpackages; $i++) { $namespaces[] = "ns$i"; - $pkgversionid = package_provider(['namespace' => "ns$i"])->store(); + [, [$pkgversionid]] = package_versions_info_provider(['namespace' => "ns$i"])->upsert(); $this->modify_package_creation_time($pkgversionid, $i); } @@ -545,7 +494,7 @@ public static function total_packages_and_limit_provider(): array { */ public function test_limit_and_offset(int $limit, int $totalpackages): void { for ($i = 0; $i < $totalpackages; $i++) { - package_provider(['namespace' => "ns$i"])->store(); + package_versions_info_provider(['namespace' => "ns$i"])->upsert(); } // Calculate the amount of full pages and the size of the page after the last full page. @@ -632,7 +581,7 @@ public function test_date_sort_with_recentlyused_category(): void { $ids = []; for ($i = 0; $i < $totalpackages; $i++) { $namespaces[] = "ns$i"; - $pkgversionid = package_provider(['namespace' => "ns$i"])->store(); + [, [$pkgversionid]] = package_versions_info_provider(['namespace' => "ns$i"])->upsert(); $ids[] = $pkgversionid; $this->modify_package_creation_time($pkgversionid, $i); } @@ -707,9 +656,9 @@ public function test_recently_used_package_not_available_across_different_course $this->getDataGenerator()->enrol_user($user->id, $course2->id); // Create package in one course and use it. - $id1 = package_provider(['namespace' => 'ns1'])->store(); + [, [$id1]] = package_versions_info_provider(['namespace' => "ns1"])->upsert(); self::add_last_used_entry($id1, $course1context->id); - $id2 = package_provider(['namespace' => 'ns2'])->store(); + [, [$id2]] = package_versions_info_provider(['namespace' => "ns2"])->upsert(); self::add_last_used_entry($id2, $course2context->id); // Execute service with both context ids. @@ -743,53 +692,41 @@ private static function favourite(user_favourite_service $ufservice, context_use } /** - * Tests that only packages marked as favourite are returned. Server provided and user uploaded packages are used. - * It also tests that the `isfavourite`-property is set correctly. + * Tests that only packages marked as favourite are returned. It also tests that the `isfavourite`-property is set correctly. * * @covers \qtype_questionpy\external\search_packages::execute * @return void * @throws moodle_exception */ public function test_favourites_returns_packages_marked_as_favourite_and_isfavourite_is_set_correctly(): void { - // Create two users and assign them to the same course. + // Create a user and assign them to the same course. $course = $this->getDataGenerator()->create_course(); - $user1 = $this->getDataGenerator()->create_and_enrol($course); - $user2 = $this->getDataGenerator()->create_and_enrol($course); - - // Create two packages provided by the server. - $pkgversionidserver = package_provider(['namespace' => 'ns1'])->store(); - package_provider(['namespace' => 'ns2'])->store(); - - // Upload two packages as user one. - $this->setUser($user1); - $pkgversionidotheruser = package_provider(['namespace' => 'ns3'])->store(); - package_provider(['namespace' => 'ns4'])->store(); + $user = $this->getDataGenerator()->create_and_enrol($course); + $this->setUser($user); - // Set user two and upload two packages. - $this->setUser($user2); - $pkgversioniduser = package_provider(['namespace' => 'ns5'])->store(); - package_provider(['namespace' => 'ns6'])->store(); + // Create two packages. + [, [$pkgversionid]] = package_versions_info_provider(['namespace' => "ns1"])->upsert(); + package_versions_info_provider(['namespace' => "ns2"])->upsert(); - // Favourite one package of each kind as user two. - $usercontext = context_user::instance($user2->id); + // Favourite one package. + $usercontext = context_user::instance($user->id); $ufservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); - self::favourite($ufservice, $usercontext, $pkgversionidserver, $pkgversionidotheruser, $pkgversioniduser); - $favourites = ['ns1', 'ns3', 'ns5']; + self::favourite($ufservice, $usercontext, $pkgversionid); // Check if only packages marked as favourite are returned. - $res = search_packages::execute('', [], 'favourites', 'alpha', 'asc', 3, 0, null); + $res = search_packages::execute('', [], 'favourites', 'alpha', 'asc', 1, 0, null); $res = external_api::clean_returnvalue(search_packages::execute_returns(), $res); - $this->assert_count_and_total($res, 3, 3); + $this->assert_count_and_total($res, 1, 1); $namespaces = array_column($res['packages'], 'namespace'); - $this->assertEqualsCanonicalizing($favourites, $namespaces); + $this->assertEquals(['ns1'], $namespaces); // Check if isfavourite-property is set correctly. - $res = search_packages::execute('', [], 'all', 'alpha', 'asc', 6, 0, null); + $res = search_packages::execute('', [], 'all', 'alpha', 'asc', 2, 0, null); $res = external_api::clean_returnvalue(search_packages::execute_returns(), $res); - $this->assert_count_and_total($res, 6, 6); + $this->assert_count_and_total($res, 2, 2); foreach ($res['packages'] as $package) { - if (in_array($package['namespace'], $favourites)) { + if ($package['namespace'] == 'ns1') { self::assertTrue($package['isfavourite']); } else { self::assertFalse($package['isfavourite']); @@ -815,8 +752,8 @@ public function test_favourites_are_not_shared_across_users(): void { $user2service = \core_favourites\service_factory::get_service_for_user_context($user2context); // Create two server packages. - $pkgversion1 = package_provider(['namespace' => 'ns1'])->store(); - $pkgversion2 = package_provider(['namespace' => 'ns2'])->store(); + [, [$pkgversion1]] = package_versions_info_provider(['namespace' => "ns1"])->upsert(); + [, [$pkgversion2]] = package_versions_info_provider(['namespace' => "ns2"])->upsert(); // Both users favourite different packages. self::favourite($user1service, $user1context, $pkgversion1); @@ -842,8 +779,8 @@ public function test_favourites_are_not_shared_across_users(): void { public function test_search_with_tags(): void { global $DB; - package_provider(['namespace' => 'ns1', 'tags' => ['a']])->store(); - package_provider(['namespace' => 'ns2', 'tags' => ['a', 'b']])->store(); + package_versions_info_provider(['namespace' => 'ns1', 'tags' => ['a']])->upsert(); + package_versions_info_provider(['namespace' => 'ns2', 'tags' => ['a', 'b']])->upsert(); $taga = $DB->get_field('qtype_questionpy_tag', 'id', ['tag' => 'a']); $tagb = $DB->get_field('qtype_questionpy_tag', 'id', ['tag' => 'b']); diff --git a/tests/package/package_raw_test.php b/tests/package/package_raw_test.php index b4bed6ac..a50fcc77 100644 --- a/tests/package/package_raw_test.php +++ b/tests/package/package_raw_test.php @@ -18,7 +18,6 @@ use moodle_exception; use qtype_questionpy\array_converter\array_converter; -use function qtype_questionpy\package_provider; defined('MOODLE_INTERNAL') || die; @@ -143,138 +142,4 @@ public function test_faulty_from_array(): void { $faulty = ['faulty' => 'hash']; array_converter::from_array(package_raw::class, $faulty); } - - - /** - * Tests the store method. - * - * @covers \package::store - * @dataProvider valid_package_data_provider - * @param array $packagedata - * @return void - * @throws moodle_exception - */ - public function test_store_package($packagedata): void { - global $DB; - $this->resetAfterTest(); - - // Create and set example user. - $user = $this->getDataGenerator()->create_user(); - $this->setUser($user); - - $timestamp = time(); - - $rawpackage = array_converter::from_array(package_raw::class, $packagedata); - $rawpackage->store(); - - // Check qtype_questionpy_pkgversion table. - $this->assertEquals(1, $DB->count_records('qtype_questionpy_pkgversion')); - $record = $DB->get_record('qtype_questionpy_pkgversion', ['hash' => $packagedata['package_hash']]); - $this->assertNotFalse($record); - $this->assertEquals($packagedata['version'], $record->version); - $packageid = $record->packageid; - - // Check qtype_questionpy_package table. - $this->assertEquals(1, $DB->count_records('qtype_questionpy_package')); - $record = $DB->get_record('qtype_questionpy_package', ['id' => $packageid]); - $this->assertNotFalse($record); - $this->assertEquals($packagedata['short_name'], $record->shortname); - $this->assertEquals($packagedata['namespace'], $record->namespace); - $this->assertEquals($packagedata['type'], $record->type); - $this->assertEquals($packagedata['author'] ?? null, $record->author); - $this->assertEquals($packagedata['url'] ?? null, $record->url); - $this->assertEquals($packagedata['icon'] ?? null, $record->icon); - $this->assertEquals($packagedata['license'] ?? null, $record->license); - $this->assertGreaterThanOrEqual($timestamp, $record->timemodified); - $this->assertGreaterThanOrEqual($timestamp, $record->timecreated); - - // Check qtype_questionpy_language table. - $languages = $packagedata['languages'] ?? []; - $this->assertEquals(count($languages), $DB->count_records('qtype_questionpy_language')); - foreach ($languages as $language) { - $record = $DB->get_record('qtype_questionpy_language', ['packageid' => $packageid, 'language' => $language]); - $this->assertNotFalse($record); - $this->assertEquals($packagedata['name'][$language], $record->name); - $this->assertEquals($packagedata['description'][$language], $record->description); - } - - // Check qtype_questionpy_tag and qtype_questionpy_pkgtag table. - $tags = $packagedata['tags'] ?? []; - $tagscount = count($tags); - $this->assertEquals($tagscount, $DB->count_records('qtype_questionpy_tag')); - $this->assertEquals($tagscount, $DB->count_records('qtype_questionpy_pkgtag')); - foreach ($tags as $tag) { - $tagid = $DB->get_field('qtype_questionpy_tag', 'id', ['tag' => $tag]); - $this->assertNotFalse($tagid); - - $record = $DB->get_record('qtype_questionpy_pkgtag', ['packageid' => $packageid, 'tagid' => $tagid]); - $this->assertNotFalse($record); - } - } - - /** - * Tests that storing same package should not throw an exception and only store it once. - * - * @covers \package::store - * @return void - * @throws moodle_exception - */ - public function test_store_package_twice(): void { - global $DB; - $this->resetAfterTest(); - - $rawpackage = package_provider(['languages' => ['en', 'de'], 'tags' => ['tag_0', 'tag_1']]); - $rawpackage->store(); - $rawpackage->store(); - - $this->assertEquals(1, $DB->count_records('qtype_questionpy_pkgversion')); - $this->assertEquals(1, $DB->count_records('qtype_questionpy_package')); - $this->assertEquals(2, $DB->count_records('qtype_questionpy_language')); - $this->assertEquals(2, $DB->count_records('qtype_questionpy_pkgtag')); - $this->assertEquals(2, $DB->count_records('qtype_questionpy_tag')); - } - - /** - * Tests that storing same package as user should throw an exception. - * - * @covers \package::store - * @return void - * @throws moodle_exception - */ - public function test_store_already_existing_package_with_different_hash_throws(): void { - $this->resetAfterTest(); - $this->expectException(moodle_exception::class); - $this->expectExceptionMessage('A package with the same version but different hash already exists.'); - - $rawpackage1 = package_provider(); - $rawpackage2 = clone $rawpackage1; - $rawpackage2->hash .= 'faulty'; - - $rawpackage1->store(); - $rawpackage2->store(); - } - - /** - * Tests the method store with multiple versions of the same package. - * - * @covers \package::store - * @return void - * @throws moodle_exception - */ - public function test_store_different_versions_of_a_package(): void { - global $DB; - $this->resetAfterTest(); - - $rawpackage1 = package_provider(['version' => '1.0.0', 'languages' => ['en'], 'tags' => ['tag_0']]); - $rawpackage2 = package_provider(['version' => '2.0.0', 'languages' => ['en'], 'tags' => ['tag_0']]); - - $rawpackage1->store(); - $rawpackage2->store(); - - $this->assertEquals(2, $DB->count_records('qtype_questionpy_pkgversion')); - $this->assertEquals(1, $DB->count_records('qtype_questionpy_package')); - $this->assertEquals(1, $DB->count_records('qtype_questionpy_language')); - $this->assertEquals(1, $DB->count_records('qtype_questionpy_pkgtag')); - $this->assertEquals(1, $DB->count_records('qtype_questionpy_tag')); - } } diff --git a/tests/package/package_test.php b/tests/package/package_test.php index bb8cc493..71d5a394 100644 --- a/tests/package/package_test.php +++ b/tests/package/package_test.php @@ -17,7 +17,7 @@ namespace qtype_questionpy\package; use moodle_exception; -use function qtype_questionpy\package_provider; +use function qtype_questionpy\package_versions_info_provider; defined('MOODLE_INTERNAL') || die; @@ -42,8 +42,7 @@ public function test_get_by_version(): void { $this->resetAfterTest(); // Store a package. - $rawpackage = package_provider(); - $pkgversionid = $rawpackage->store(); + [, [$pkgversionid]] = package_versions_info_provider()->upsert(); // Get the package. package::get_by_version($pkgversionid); @@ -83,7 +82,7 @@ public function test_delete(): void { $this->resetAfterTest(); // Store a package. - $pkgversionid = package_provider(['languages' => ['en', 'de'], 'tags' => ['a']])->store(); + [, [$pkgversionid]] = package_versions_info_provider(['languages' => ['en', 'de'], 'tags' => ['a']])->upsert(); $package = package::get_by_version($pkgversionid); // Delete the package. @@ -103,8 +102,10 @@ public function test_delete_with_multiple_versions(): void { $this->resetAfterTest(); // Store two versions of the same package. - package_provider(['version' => '1.0.0', 'languages' => ['en'], 'tags' => ['a']])->store(); - $pkgversionid = package_provider(['version' => '2.0.0', 'languages' => ['en'], 'tags' => ['a']])->store(); + [, [$pkgversionid, ]] = package_versions_info_provider( + ['languages' => ['en'], 'tags' => ['a']], + [['version' => '2.0.0'], ['version' => '1.0.0']] + )->upsert(); $package = package::get_by_version($pkgversionid); // Delete the package. @@ -124,10 +125,10 @@ public function test_delete_with_multiple_packages(): void { $this->resetAfterTest(); // Store two packages. - $package1 = package_provider(['namespace' => 'ns1', 'tags' => ['a', 'b']])->store(); + [, [$package1, ]] = package_versions_info_provider(['namespace' => 'ns1', 'tags' => ['a', 'b']])->upsert(); $package1 = package::get_by_version($package1); - $package2 = package_provider(['namespace' => 'ns2', 'tags' => ['b', 'c']])->store(); + [, [$package2, ]] = package_versions_info_provider(['namespace' => 'ns2', 'tags' => ['b', 'c']])->upsert(); $package2 = package::get_by_version($package2); $package1->delete(); diff --git a/tests/package/package_version_test.php b/tests/package/package_version_test.php index 95434f67..340ddb7a 100644 --- a/tests/package/package_version_test.php +++ b/tests/package/package_version_test.php @@ -17,8 +17,7 @@ namespace qtype_questionpy\package; use moodle_exception; -use qtype_questionpy\package\package_version; -use function qtype_questionpy\package_provider; +use function qtype_questionpy\package_versions_info_provider; defined('MOODLE_INTERNAL') || die; @@ -45,7 +44,7 @@ public function test_get_by_id(): void { $hash = 'hash'; $version = '1.0.0'; - $pkgversionid = package_provider(['hash' => $hash, 'version' => $version])->store(); + [, [$pkgversionid]] = package_versions_info_provider(null, [['hash' => $hash, 'version' => $version]])->upsert(); $package = package_version::get_by_id($pkgversionid); $this->assertEquals($hash, $package->hash); @@ -66,7 +65,7 @@ public function test_get_by_hash(): void { $hash = 'hash'; $version = '1.0.0'; - package_provider(['hash' => $hash, 'version' => $version])->store(); + package_versions_info_provider(null, [['hash' => $hash, 'version' => $version]])->upsert(); $package = package_version::get_by_hash($hash); $this->assertEquals($hash, $package->hash); @@ -85,7 +84,7 @@ public function test_delete(): void { $this->resetAfterTest(); // Store a package. - $pkgversionid = package_provider()->store(); + [, [$pkgversionid]] = package_versions_info_provider()->upsert(); $package = package_version::get_by_id($pkgversionid); // Delete the package. @@ -110,10 +109,12 @@ public function test_delete_where_multiple_versions_exist(): void { $this->resetAfterTest(); // Store two packages. - $pkgversionid1 = package_provider(['version' => '1.0.0', 'languages' => ['en'], 'tags' => ['tag']])->store(); - $package1 = package_version::get_by_id($pkgversionid1); + [, [$pkgversionid1, $pkgversionid2]] = package_versions_info_provider( + ['languages' => ['en'], 'tags' => ['tag']], + [['version' => '2.0.0'], ['version' => '1.0.0']] + )->upsert(); - $pkgversionid2 = package_provider(['version' => '2.0.0', 'languages' => ['de'], 'tags' => ['tag']])->store(); + $package1 = package_version::get_by_id($pkgversionid1); $package2 = package_version::get_by_id($pkgversionid2); // Delete the first package. diff --git a/tests/package/package_versions_info_test.php b/tests/package/package_versions_info_test.php new file mode 100644 index 00000000..06c53d78 --- /dev/null +++ b/tests/package/package_versions_info_test.php @@ -0,0 +1,170 @@ +. + +namespace qtype_questionpy\package; + +use moodle_exception; +use function qtype_questionpy\package_versions_info_provider; + +defined('MOODLE_INTERNAL') || die; + +require_once(dirname(__DIR__) . '/data_provider.php'); + +/** + * Unit tests for the questionpy package_versions_info class. + * + * @package qtype_questionpy + * @copyright 2024 Jan Britz, TU Berlin, innoCampus - www.questionpy.org + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +final class package_versions_info_test extends \advanced_testcase { + /** + * Adds package versions to the database. + * + * @param array $versions + * @return package_versions_info + * @throws moodle_exception + */ + private function get_package_versions_info(array $versions): package_versions_info { + $prepared = []; + foreach ($versions as $version) { + $prepared[] = ['version' => $version]; + } + return package_versions_info_provider(null, $prepared); + } + + /** + * Returns the database records of the package versions. + * + * @param package_versions_info $pvi + * @return array + * @throws moodle_exception + */ + private function get_version_records(package_versions_info $pvi): array { + global $DB; + [$sql, $params] = $DB->get_in_or_equal(array_column($pvi->versions, 'hash')); + return $DB->get_records_select('qtype_questionpy_pkgversion', "hash $sql", $params, 'versionorder'); + } + + /** + * Tests that the package record is modified if the latest version does change. + * + * @covers \qtype_questionpy\package\package_version_specific_info::upsert + * @throws moodle_exception + */ + public function test_package_data_gets_updated_if_newer_versions_are_added_or_removed(): void { + global $DB; + $this->resetAfterTest(); + + $versionsarray = [['0.0.1'], ['1.0.0', '0.0.1'], ['0.0.1']]; + + foreach ($versionsarray as $time => $versions) { + $pvi = $this->get_package_versions_info($versions); + $pvi->upsert($time); + + $record = $DB->get_record('qtype_questionpy_package', ['namespace' => $pvi->manifest->namespace]); + $this->assertEquals($time, $record->timemodified); + } + + $this->assertEquals(0, $record->timecreated); + } + + /** + * Tests that the package record is not modified if the latest version does not change. + * + * @covers \qtype_questionpy\package\package_version_specific_info::upsert + * @throws moodle_exception + */ + public function test_package_data_does_not_get_updated_if_only_older_versions_are_added_or_removed(): void { + global $DB; + $this->resetAfterTest(); + + $versionsarray = [['1.0.0'], ['1.0.0', '0.1.0'], ['1.0.0', '0.0.1'], ['1.0.0']]; + + foreach ($versionsarray as $time => $versions) { + $pvi = $this->get_package_versions_info($versions); + $pvi->upsert($time); + } + + $record = $DB->get_record('qtype_questionpy_package', ['namespace' => $pvi->manifest->namespace]); + $this->assertEquals(0, $record->timecreated); + $this->assertEquals(0, $record->timemodified); + } + + /** + * Tests that the version records are correctly modified if the versions are changed. + * + * @covers \qtype_questionpy\package\package_version_specific_info::upsert + * @throws moodle_exception + */ + public function test_versions_get_updated_if_there_are_changes(): void { + global $DB; + $this->resetAfterTest(); + + $expectedtimecreatedmodified = [ + ['0.1.0' => [0, 0]], + ['1.0.0' => [1, 1], '0.1.0' => [0, 1]], + ['1.0.0' => [1, 2], '0.1.0' => [0, 2], '0.0.1' => [2, 2]], + ['2.0.0' => [3, 3]], + ]; + + foreach ($expectedtimecreatedmodified as $time => $expected) { + $versions = array_keys($expected); + $pvi = $this->get_package_versions_info($versions); + $pvi->upsert($time); + $records = $this->get_version_records($pvi); + + // Check that the versions are sorted correctly/the versionorder-field is set correctly. + $this->assertEquals($versions, array_column($records, 'version')); + $this->assertEquals(range(0, count($versions) - 1), array_column($records, 'versionorder')); + + $this->assertEquals(count($versions), $DB->count_records('qtype_questionpy_pkgversion')); + + // Check creation and modification time. + foreach ($records as $record) { + [$expectedtimecreated, $expectedtimemodified] = $expected[$record->version]; + $this->assertEquals($expectedtimecreated, $record->timecreated); + $this->assertEquals($expectedtimemodified, $record->timemodified); + } + } + } + + /** + * Tests that the version records are not modified if there are no changes. + * + * @covers \qtype_questionpy\package\package_version_specific_info::upsert + * @throws moodle_exception + */ + public function test_versions_do_not_get_updated_if_there_are_no_changes(): void { + global $DB; + $this->resetAfterTest(); + + $versions = ['1.0.0', '0.1.0']; + + $pvi = $this->get_package_versions_info($versions); + $pvi->upsert(0); + $pvi->upsert(1); + + $this->assertEquals(count($versions), $DB->count_records('qtype_questionpy_pkgversion')); + + // Check creation and modification time. + $records = $this->get_version_records($pvi); + foreach ($records as $record) { + $this->assertEquals(0, $record->timecreated); + $this->assertEquals(0, $record->timemodified); + } + } +} diff --git a/tests/question_service_test.php b/tests/question_service_test.php index 639f4169..5cb6ccf7 100644 --- a/tests/question_service_test.php +++ b/tests/question_service_test.php @@ -45,8 +45,6 @@ final class question_service_test extends \advanced_testcase { protected function setUp(): void { $this->api = $this->createMock(api::class); - $this->api->method("get_package") - ->willReturn(null); $this->packageapi = $this->createMock(package_api::class); $this->api->method("package") ->willReturn($this->packageapi); @@ -64,16 +62,16 @@ protected function setUp(): void { public function test_get_question_should_load_package_and_state(): void { $this->resetAfterTest(); - $package = package_provider(); - $pkgversionid = $package->store(); - [$statestr, $qpyid] = $this->setup_question($package->hash, $pkgversionid); + $pvi = package_versions_info_provider(); + [, [$pkgversionid]] = $pvi->upsert(); + [$statestr, $qpyid] = $this->setup_question($pvi->versions[0]->hash, $pkgversionid); $result = $this->questionservice->get_question(1); $this->assertEquals( (object)[ "qpy_id" => $qpyid, - "qpy_package_hash" => $package->hash, + "qpy_package_hash" => $pvi->versions[0]->hash, "qpy_state" => $statestr, "qpy_is_local" => "0", ], @@ -104,12 +102,10 @@ public function test_upsert_question_should_update_existing_record_if_changed(): global $PAGE; $this->resetAfterTest(); - $oldpackage = package_provider(["version" => "0.1.0"]); - $oldpackageid = $oldpackage->store(); - $oldstate = $this->setup_question($oldpackage->hash, $oldpackageid)[0]; + $pvi = package_versions_info_provider(null, [["version" => "0.2.0"], ["version" => "0.1.0"]]); + [, [$newpackageid, $oldpackageid]] = $pvi->upsert(); - $newpackage = package_provider(["version" => "0.2.0"]); - $newpackageid = $newpackage->store(); + $oldstate = $this->setup_question($pvi->versions[1]->hash, $oldpackageid)[0]; $newstate = json_encode(["this is" => "new state"]); $formdata = ["this is" => "form data"]; @@ -123,7 +119,7 @@ public function test_upsert_question_should_update_existing_record_if_changed(): $this->questionservice->upsert_question( (object)[ "id" => 1, - "qpy_package_hash" => $newpackage->hash, + "qpy_package_hash" => $pvi->versions[0]->hash, "qpy_form" => $formdata, "qpy_package_source" => "search", "oldparent" => 1, @@ -146,10 +142,10 @@ public function test_upsert_question_should_do_nothing_if_unchanged(): void { global $PAGE; $this->resetAfterTest(); - $package = package_provider(); - $packageid = $package->store(); + $pvi = package_versions_info_provider(); + [, [$pkgversionid]] = $pvi->upsert(); - $oldstate = $this->setup_question($package->hash, $packageid)[0]; + $oldstate = $this->setup_question($pvi->versions[0]->hash, $pkgversionid)[0]; $formdata = ["this is" => "form data"]; @@ -162,7 +158,7 @@ public function test_upsert_question_should_do_nothing_if_unchanged(): void { $this->questionservice->upsert_question( (object)[ "id" => 1, - "qpy_package_hash" => $package->hash, + "qpy_package_hash" => $pvi->versions[0]->hash, "qpy_form" => $formdata, "qpy_package_source" => "search", "oldparent" => 1, @@ -170,7 +166,7 @@ public function test_upsert_question_should_do_nothing_if_unchanged(): void { ] ); - $this->assert_single_question(1, $packageid, $oldstate); + $this->assert_single_question(1, $pkgversionid, $oldstate); } /** @@ -186,8 +182,8 @@ public function test_upsert_question_should_insert_record(): void { global $PAGE; $this->resetAfterTest(); - $package = package_provider(); - $packageid = $package->store(); + $pvi = package_versions_info_provider(); + [, [$pkgversionid]] = $pvi->upsert(); $newstate = json_encode(["this is" => "new state"]); $formdata = ["this is" => "form data"]; @@ -201,7 +197,7 @@ public function test_upsert_question_should_insert_record(): void { $this->questionservice->upsert_question( (object)[ "id" => 42, // Does not exist in the qtype_questionpy table yet. - "qpy_package_hash" => $package->hash, + "qpy_package_hash" => $pvi->versions[0]->hash, "qpy_form" => $formdata, "qpy_package_source" => "search", "oldparent" => 1, @@ -209,7 +205,7 @@ public function test_upsert_question_should_insert_record(): void { ] ); - $this->assert_single_question(42, $packageid, $newstate); + $this->assert_single_question(42, $pkgversionid, $newstate); } /** @@ -246,8 +242,8 @@ public function test_upsert_question_should_add_package_to_last_used_table(): vo global $DB, $PAGE; $this->resetAfterTest(); - $rawpackage = package_provider(); - $pkgversionid = $rawpackage->store(); + $pvi = package_versions_info_provider(); + [, [$pkgversionid]] = $pvi->upsert(); $package = package::get_by_version($pkgversionid); @@ -263,7 +259,7 @@ public function test_upsert_question_should_add_package_to_last_used_table(): vo $this->questionservice->upsert_question( (object)[ "id" => 42, // Does not exist in the qtype_questionpy table yet. - "qpy_package_hash" => $rawpackage->hash, + "qpy_package_hash" => $pvi->versions[0]->hash, "qpy_form" => $formdata, "qpy_package_source" => "search", "oldparent" => 1, @@ -289,8 +285,8 @@ public function test_upsert_question_in_same_context_with_same_package_should_on global $DB, $PAGE; $this->resetAfterTest(); - $rawpackage = package_provider(); - $pkgversionid = $rawpackage->store(); + $pvi = package_versions_info_provider(); + [, [$pkgversionid]] = $pvi->upsert(); $package = package::get_by_version($pkgversionid); @@ -306,7 +302,7 @@ public function test_upsert_question_in_same_context_with_same_package_should_on $this->questionservice->upsert_question( (object)[ "id" => 42, // Does not exist in the qtype_questionpy table yet. - "qpy_package_hash" => $rawpackage->hash, + "qpy_package_hash" => $pvi->versions[0]->hash, "qpy_form" => $formdata, "qpy_package_source" => "search", "oldparent" => 1, @@ -323,7 +319,7 @@ public function test_upsert_question_in_same_context_with_same_package_should_on $this->questionservice->upsert_question( (object)[ "id" => 43, // Does not exist in the qtype_questionpy table yet. - "qpy_package_hash" => $rawpackage->hash, + "qpy_package_hash" => $pvi->versions[0]->hash, "qpy_form" => $formdata, "qpy_package_source" => "search", "oldparent" => 1, @@ -351,9 +347,9 @@ public function test_upsert_question_in_same_context_with_same_package_should_on public function test_delete_question(): void { $this->resetAfterTest(); - $package = package_provider(); - $pkgversionid = $package->store(); - $this->setup_question($package->hash, $pkgversionid); + $pvi = package_versions_info_provider(); + [, [$pkgversionid]] = $pvi->upsert(); + $this->setup_question($pvi->versions[0]->hash, $pkgversionid); global $DB; $this->assertEquals(1, $DB->count_records("qtype_questionpy")); diff --git a/version.php b/version.php index 24c8b54e..cd0f2557 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'qtype_questionpy'; -$plugin->version = 2024072900; +$plugin->version = 2024073100; $plugin->requires = 2024042200; $plugin->maturity = MATURITY_ALPHA; $plugin->release = '0.1';