From d5baebeae31f68297bf968352dbd35135595f64b Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 8 Jul 2024 09:09:03 +0200 Subject: [PATCH 1/7] Add behat tests for User::list() --- tests/Behat/Bootstrap/UserContextTrait.php | 34 +++++-- tests/Behat/features/user.feature | 111 +++++++++++++++++---- 2 files changed, 116 insertions(+), 29 deletions(-) diff --git a/tests/Behat/Bootstrap/UserContextTrait.php b/tests/Behat/Bootstrap/UserContextTrait.php index f6b55458..a5244682 100644 --- a/tests/Behat/Bootstrap/UserContextTrait.php +++ b/tests/Behat/Bootstrap/UserContextTrait.php @@ -30,35 +30,49 @@ public function iCreateAUserWithTheFollowingData(TableNode $table) } /** - * @When I update the user with id :id and the following data + * @When I show the user with id :userId */ - public function iUpdateTheUserWithIdAndTheFollowingData($id, TableNode $table) + public function iShowTheUserWithId(int $userId) { - $data = []; + /** @var User */ + $api = $this->getNativeCurlClient()->getApi('user'); - foreach ($table as $row) { - $data[$row['property']] = $row['value']; - } + $this->registerClientResponse( + $api->show($userId), + $api->getLastResponse(), + ); + } + /** + * @When I list all users + */ + public function iListAllUsers() + { /** @var User */ $api = $this->getNativeCurlClient()->getApi('user'); $this->registerClientResponse( - $api->update($id, $data), + $api->list(), $api->getLastResponse(), ); } /** - * @When I show the user with id :userId + * @When I update the user with id :id and the following data */ - public function iShowTheUserWithId(int $userId) + public function iUpdateTheUserWithIdAndTheFollowingData($id, TableNode $table) { + $data = []; + + foreach ($table as $row) { + $data[$row['property']] = $row['value']; + } + /** @var User */ $api = $this->getNativeCurlClient()->getApi('user'); $this->registerClientResponse( - $api->show($userId), + $api->update($id, $data), $api->getLastResponse(), ); } diff --git a/tests/Behat/features/user.feature b/tests/Behat/features/user.feature index 367f03c9..c616f53a 100644 --- a/tests/Behat/features/user.feature +++ b/tests/Behat/features/user.feature @@ -4,7 +4,6 @@ Feature: Interacting with the REST API for users As a user I want to make sure the Redmine server replies with the correct response - Scenario: Creating an user Given I have a "NativeCurlClient" client When I create a user with the following data @@ -45,24 +44,6 @@ Feature: Interacting with the REST API for users | twofa_scheme | [] | | status | 1 | - Scenario: Updating an user - Given I have a "NativeCurlClient" client - And I create a user with the following data - | property | value | - | login | username | - | firstname | first | - | lastname | last | - | mail | mail@example.com | - When I update the user with id "5" and the following data - | property | value | - | firstname | new_first | - | lastname | new_last | - | mail | new_mail@example.com | - Then the response has the status code "204" - And the response has an empty content type - And the response has the content "" - And the returned data is exactly "" - Scenario: Showing a user Given I have a "NativeCurlClient" client When I show the user with id "1" @@ -113,6 +94,98 @@ Feature: Interacting with the REST API for users And the response has the content "" And the returned data is false + Scenario: Listing of multiple users + Given I have a "NativeCurlClient" client + And I create a user with the following data + | property | value | + | login | username | + | firstname | first | + | lastname | last | + | mail | mail@example.net | + When I list all users + Then the response has the status code "200" + And the response has the content type "application/json" + And the returned data has only the following properties + """ + users + total_count + offset + limit + """ + And the returned data has proterties with the following data + | property | value | + | total_count | 2 | + | offset | 0 | + | limit | 25 | + And the returned data "users" property is an array + And the returned data "users" property contains "2" items + And the returned data "users.0" property is an array + And the returned data "users.0" property has only the following properties + """ + id + login + admin + firstname + lastname + mail + created_on + updated_on + last_login_on + passwd_changed_on + twofa_scheme + """ + And the returned data "users.0" property contains the following data + | property | value | + | id | 1 | + | login | admin | + | admin | true | + | firstname | Redmine | + | lastname | Admin | + | mail | admin@example.net | + | twofa_scheme | null | + And the returned data "users.1" property is an array + And the returned data "users.1" property has only the following properties + """ + id + login + admin + firstname + lastname + mail + created_on + updated_on + last_login_on + passwd_changed_on + twofa_scheme + """ + And the returned data "users.1" property contains the following data + | property | value | + | id | 5 | + | login | username | + | admin | false | + | firstname | first | + | lastname | last | + | mail | mail@example.net | + | twofa_scheme | null | + + Scenario: Updating an user + Given I have a "NativeCurlClient" client + And I create a user with the following data + | property | value | + | login | username | + | firstname | first | + | lastname | last | + | mail | mail@example.com | + When I update the user with id "5" and the following data + | property | value | + | firstname | new_first | + | lastname | new_last | + | mail | new_mail@example.com | + Then the response has the status code "204" + And the response has an empty content type + And the response has the content "" + And the returned data is exactly "" + Scenario: Removing an user Given I have a "NativeCurlClient" client And I create a user with the following data From 88effb994f264b79f495e92012ae903032f4c944 Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 8 Jul 2024 09:52:11 +0200 Subject: [PATCH 2/7] Add User::listLogins() --- CHANGELOG.md | 1 + src/Redmine/Api/User.php | 39 +++++ tests/Behat/Bootstrap/UserContextTrait.php | 34 +++++ tests/Behat/features/user.feature | 25 +++ tests/Unit/Api/User/ListLoginsTest.php | 167 +++++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 tests/Unit/Api/User/ListLoginsTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ca827ca..40938b48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New method `Redmine\Api\Project::listNames()` for listing the ids and names of all projects. - New method `Redmine\Api\Role::listNames()` for listing the ids and names of all roles. - New method `Redmine\Api\TimeEntryActivity::listNames()` for listing the ids and names of all time entry activities. +- New method `Redmine\Api\User::listLogins()` for listing the ids and logins of all users. ### Deprecated diff --git a/src/Redmine/Api/User.php b/src/Redmine/Api/User.php index aa93cfe1..005825bb 100644 --- a/src/Redmine/Api/User.php +++ b/src/Redmine/Api/User.php @@ -23,6 +23,8 @@ class User extends AbstractApi { private $users = []; + private $userLogins = null; + /** * List users. * @@ -43,6 +45,43 @@ final public function list(array $params = []): array } } + /** + * Returns an array of all users with id/login pairs. + * + * @return array list of users (id => login) + */ + final public function listLogins(): array + { + if ($this->userLogins !== null) { + return $this->userLogins; + } + + $this->userLogins = []; + + $limit = 100; + $offset = 0; + + do { + $list = $this->list([ + 'limit' => $limit, + 'offset' => $offset, + ]); + + $listCount = 0; + $offset += $limit; + + if (array_key_exists('users', $list)) { + $listCount = count($list['users']); + + foreach ($list['users'] as $user) { + $this->userLogins[(int) $user['id']] = (string) $user['login']; + } + } + } while ($listCount === $limit); + + return $this->userLogins; + } + /** * List users. * diff --git a/tests/Behat/Bootstrap/UserContextTrait.php b/tests/Behat/Bootstrap/UserContextTrait.php index a5244682..7ee1a4ed 100644 --- a/tests/Behat/Bootstrap/UserContextTrait.php +++ b/tests/Behat/Bootstrap/UserContextTrait.php @@ -9,6 +9,26 @@ trait UserContextTrait { + /** + * @Given I create :count users + */ + public function iCreateUsers(int $count) + { + while ($count > 0) { + $table = new TableNode([ + ['property', 'value'], + ['login', 'testuser_' . $count], + ['firstname', 'first'], + ['lastname', 'last'], + ['mail', 'mail.' . $count . '@example.net'], + ]); + + $this->iCreateAUserWithTheFollowingData($table); + + $count--; + } + } + /** * @When I create a user with the following data */ @@ -57,6 +77,20 @@ public function iListAllUsers() ); } + /** + * @When I list all user logins + */ + public function iListAllUserLogins() + { + /** @var User */ + $api = $this->getNativeCurlClient()->getApi('user'); + + $this->registerClientResponse( + $api->listLogins(), + $api->getLastResponse(), + ); + } + /** * @When I update the user with id :id and the following data */ diff --git a/tests/Behat/features/user.feature b/tests/Behat/features/user.feature index c616f53a..b1562081 100644 --- a/tests/Behat/features/user.feature +++ b/tests/Behat/features/user.feature @@ -168,6 +168,31 @@ Feature: Interacting with the REST API for users | mail | mail@example.net | | twofa_scheme | null | + Scenario: Listing of multiple user logins + Given I have a "NativeCurlClient" client + And I create a user with the following data + | property | value | + | login | username | + | firstname | first | + | lastname | last | + | mail | mail@example.net | + When I list all user logins + Then the response has the status code "200" + And the response has the content type "application/json" + And the returned data contains "2" items + And the returned data has proterties with the following data + | property | value | + | 1 | admin | + | 5 | username | + + Scenario: Listing of multiple user logins + Given I have a "NativeCurlClient" client + And I create "108" users + When I list all user logins + Then the response has the status code "200" + And the response has the content type "application/json" + And the returned data contains "109" items + Scenario: Updating an user Given I have a "NativeCurlClient" client And I create a user with the following data diff --git a/tests/Unit/Api/User/ListLoginsTest.php b/tests/Unit/Api/User/ListLoginsTest.php new file mode 100644 index 00000000..23e0643e --- /dev/null +++ b/tests/Unit/Api/User/ListLoginsTest.php @@ -0,0 +1,167 @@ +assertSame($expectedResponse, $api->listLogins()); + } + + public static function getListLoginsData(): array + { + return [ + 'test without users' => [ + '/users.json?limit=100&offset=0', + 201, + << [ + '/users.json?limit=100&offset=0', + 201, + << "username_C", + 8 => "username_B", + 9 => "username_A", + ], + ], + ]; + } + + public function testListLoginsWithALotOfUsersHandlesPagination() + { + $assertData = []; + $usersRequest1 = []; + $usersRequest2 = []; + $usersRequest3 = []; + + for ($i = 1; $i <= 100; $i++) { + $login = 'user_' . $i; + + $assertData[$i] = $login; + $usersRequest1[] = ['id' => $i, 'login' => $login]; + } + + for ($i = 101; $i <= 200; $i++) { + $login = 'user_' . $i; + + $assertData[$i] = $login; + $usersRequest2[] = ['id' => $i, 'login' => $login]; + } + + $client = AssertingHttpClient::create( + $this, + [ + 'GET', + '/users.json?limit=100&offset=0', + 'application/json', + '', + 200, + 'application/json', + json_encode(['users' => $usersRequest1]), + ], + [ + 'GET', + '/users.json?limit=100&offset=100', + 'application/json', + '', + 200, + 'application/json', + json_encode(['users' => $usersRequest2]), + ], + [ + 'GET', + '/users.json?limit=100&offset=200', + 'application/json', + '', + 200, + 'application/json', + json_encode(['users' => $usersRequest3]), + ], + ); + + // Create the object under test + $api = new User($client); + + // Perform the tests + $this->assertSame($assertData, $api->listLogins()); + } + + public function testListLoginsCallsHttpClientOnlyOnce() + { + $client = AssertingHttpClient::create( + $this, + [ + 'GET', + '/users.json?limit=100&offset=0', + 'application/json', + '', + 200, + 'application/json', + <<assertSame([1 => 'username'], $api->listLogins()); + $this->assertSame([1 => 'username'], $api->listLogins()); + $this->assertSame([1 => 'username'], $api->listLogins()); + } +} From 154903f57b0a7d4ac6be7d456e9155d03895028f Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 8 Jul 2024 11:15:01 +0200 Subject: [PATCH 3/7] Deprecate User::listing() --- CHANGELOG.md | 1 + src/Redmine/Api/User.php | 5 +++++ tests/Unit/Api/UserTest.php | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40938b48..cab1d42b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Redmine\Api\Project::listing()` is deprecated, use `\Redmine\Api\Project::listNames()` instead. - `Redmine\Api\Role::listing()` is deprecated, use `\Redmine\Api\Role::listNames()` instead. - `Redmine\Api\TimeEntryActivity::listing()` is deprecated, use `\Redmine\Api\TimeEntryActivity::listNames()` instead. +- `Redmine\Api\User::listing()` is deprecated, use `\Redmine\Api\User::listLogins()` instead. ## [v2.6.0](https://github.com/kbsali/php-redmine-api/compare/v2.5.0...v2.6.0) - 2024-03-25 diff --git a/src/Redmine/Api/User.php b/src/Redmine/Api/User.php index 005825bb..d6d52d3f 100644 --- a/src/Redmine/Api/User.php +++ b/src/Redmine/Api/User.php @@ -118,6 +118,9 @@ public function all(array $params = []) /** * Returns an array of users with login/id pairs. * + * @deprecated v2.7.0 Use listLogins() instead. + * @see User::listLogins() + * * @param bool $forceUpdate to force the update of the users var * @param array $params to allow offset/limit (and more) to be passed * @@ -125,6 +128,8 @@ public function all(array $params = []) */ public function listing($forceUpdate = false, array $params = []) { + @trigger_error('`' . __METHOD__ . '()` is deprecated since v2.7.0, use `' . __CLASS__ . '::listLogins()` instead.', E_USER_DEPRECATED); + if (empty($this->users) || $forceUpdate) { $this->users = $this->list($params); } diff --git a/tests/Unit/Api/UserTest.php b/tests/Unit/Api/UserTest.php index cc781a1c..3dc27123 100644 --- a/tests/Unit/Api/UserTest.php +++ b/tests/Unit/Api/UserTest.php @@ -286,4 +286,36 @@ public function testListingCallsGetEveryTimeWithForceUpdate() $this->assertSame($expectedReturn, $api->listing(true)); $this->assertSame($expectedReturn, $api->listing(true)); } + + /** + * Test listing(). + */ + public function testListingTriggersDeprecationWarning() + { + $client = $this->createMock(Client::class); + $client->method('requestGet') + ->willReturn(true); + $client->method('getLastResponseBody') + ->willReturn('{"users":[{"id":1,"login":"user_1"},{"id":5,"login":"user_5"}]}'); + $client->method('getLastResponseContentType') + ->willReturn('application/json'); + + $api = new User($client); + + // PHPUnit 10 compatible way to test trigger_error(). + set_error_handler( + function ($errno, $errstr): bool { + $this->assertSame( + '`Redmine\Api\User::listing()` is deprecated since v2.7.0, use `Redmine\Api\User::listLogins()` instead.', + $errstr, + ); + + restore_error_handler(); + return true; + }, + E_USER_DEPRECATED, + ); + + $api->listing(); + } } From e5028245cdf07b4b416a60a354d1e63dc3a22337 Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 8 Jul 2024 11:35:15 +0200 Subject: [PATCH 4/7] create new testing command for BDT, improve docs --- composer.json | 7 ++++++- tests/Behat/README.md | 40 ++++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/composer.json b/composer.json index 01382ecd..f90b7334 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,12 @@ "sort-packages": true }, "scripts": { - "behat": "behat --config tests/Behat/behat.yml --format progress", + "bdt": [ + "@behat --format=progress --suite=redmine_50103", + "@behat --format=progress --suite=redmine_50009", + "@behat --format=progress --suite=redmine_40210" + ], + "behat": "behat --config tests/Behat/behat.yml", "codestyle": "php-cs-fixer fix", "coverage": "phpunit --coverage-html=\".phpunit.cache/code-coverage\"", "phpstan": "phpstan analyze --memory-limit 512M --configuration .phpstan.neon", diff --git a/tests/Behat/README.md b/tests/Behat/README.md index 4920052d..b9c9c8a9 100644 --- a/tests/Behat/README.md +++ b/tests/Behat/README.md @@ -10,27 +10,35 @@ Pull the Redmine docker images and start them by running: docker compose up -d ``` -Now you can run the tests: +Now you can run all behaviour-driven tests grouped by Redmine version using the `bdt` command: ```bash # all tests -docker compose exec php composer behat -# only a specific redmine version +docker compose exec php composer bdt +``` + +If you need more control about the behat tests or want to change the output format, +you can use the `behat` command directly: + +```bash +# test only a specific redmine version docker compose exec php composer behat -- --suite=redmine_50103 -# only specific endpoints +# test only specific endpoints docker compose exec php composer behat -- --tags=issue,group -# only specific endpoints on a specific redmine version +# test only specific endpoints on a specific redmine version docker compose exec php composer behat -- --suite=redmine_50103 --tags=issue,group +# test only a specific redmine version and format the output as `progress` (default is `pretty`) +docker compose exec php composer behat -- --suite=redmine_50103 --format=progress ``` -## Redmine specific features +## Redmine version specific features -Some Redmine features are specific for a Redmine version. There are two ways to handle this situations. +Some Redmine features are specific for a Redmine version. Theses situations are handled in different ways. ### Modified Rest-API Responses It is possible that a new Redmine version returns new or changed elements in a response. -This can be handled on the `step` layer: +This can be handled on the `step` layer (note the missing `homepage` property in `< 5.1.0`): ``` And the returned data "projects.0" property has only the following properties with Redmine version ">= 5.1.0" @@ -40,11 +48,6 @@ This can be handled on the `step` layer: identifier description homepage - status - is_public - inherit_members - created_on - updated_on """ But the returned data "projects.0" property has only the following properties with Redmine version "< 5.1.0" """ @@ -52,17 +55,13 @@ This can be handled on the `step` layer: name identifier description - status - is_public - inherit_members - created_on - updated_on """ ``` ### New Rest-API Endpoints -A new Redmine version could be introduce new REST-API endpoints that are missing in the older version. +A new Redmine version could introduce new REST-API endpoints. +Tests for this endpoint should not be run on older Redmine versions. This can be handled on the `scenario` or `feature` layer. 1. Tag features or scenarios e.g. with `@since50000`. @@ -98,7 +97,8 @@ default: ### Removed Rest-API Endpoints -A new Redmine version could remove REST-API endpoints that are missing in the newer versions. +A new Redmine version could remove REST-API endpoints. +Tests for this endpoint should not be run on newer Redmine versions. This can be handled on the `scenario` or `feature` layer. 1. Tag features or scenarios e.g. with `@until60000`. From 20820d145bd2dcd49a9494e34e788b61d92c1d18 Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 8 Jul 2024 13:23:19 +0200 Subject: [PATCH 5/7] add behat tests for Version::listByProject() --- tests/Behat/Bootstrap/VersionContextTrait.php | 34 ++++--- tests/Behat/features/version.feature | 89 ++++++++++++++++--- 2 files changed, 99 insertions(+), 24 deletions(-) diff --git a/tests/Behat/Bootstrap/VersionContextTrait.php b/tests/Behat/Bootstrap/VersionContextTrait.php index 13c9d9ba..b8798721 100644 --- a/tests/Behat/Bootstrap/VersionContextTrait.php +++ b/tests/Behat/Bootstrap/VersionContextTrait.php @@ -45,35 +45,49 @@ public function iCreateAVersionWithProjectIdentifierAndWithTheFollowingData(stri } /** - * @When I update the version with id :id and the following data + * @When I show the version with id :versionId */ - public function iUpdateTheVersionWithIdAndTheFollowingData($id, TableNode $table) + public function iShowTheVersionWithId(int $versionId) { - $data = []; + /** @var Version */ + $api = $this->getNativeCurlClient()->getApi('version'); - foreach ($table as $row) { - $data[$row['property']] = $row['value']; - } + $this->registerClientResponse( + $api->show($versionId), + $api->getLastResponse(), + ); + } + /** + * @When I list all versions for project identifier :identifier + */ + public function iListAllVersionsForProjectIdentifier($identifier) + { /** @var Version */ $api = $this->getNativeCurlClient()->getApi('version'); $this->registerClientResponse( - $api->update($id, $data), + $api->listByProject($identifier), $api->getLastResponse(), ); } /** - * @When I show the version with id :versionId + * @When I update the version with id :id and the following data */ - public function iShowTheVersionWithId(int $versionId) + public function iUpdateTheVersionWithIdAndTheFollowingData($id, TableNode $table) { + $data = []; + + foreach ($table as $row) { + $data[$row['property']] = $row['value']; + } + /** @var Version */ $api = $this->getNativeCurlClient()->getApi('version'); $this->registerClientResponse( - $api->show($versionId), + $api->update($id, $data), $api->getLastResponse(), ); } diff --git a/tests/Behat/features/version.feature b/tests/Behat/features/version.feature index 72ac2434..3a324f6e 100644 --- a/tests/Behat/features/version.feature +++ b/tests/Behat/features/version.feature @@ -52,20 +52,6 @@ Feature: Interacting with the REST API for versions | id | 1 | | name | Test Project | - Scenario: Updating a version - Given I have a "NativeCurlClient" client - And I create a project with name "Test Project" and identifier "test-project" - And I create a version with project identifier "test-project" with the following data - | property | value | - | name | Test-Version | - When I update the version with id "1" and the following data - | property | value | - | name | New Version name | - Then the response has the status code "204" - And the response has an empty content type - And the response has the content "" - And the returned data is exactly "" - Scenario: Showing a version Given I have a "NativeCurlClient" client And I create a project with name "Test Project" and identifier "test-project" @@ -115,6 +101,81 @@ Feature: Interacting with the REST API for versions And the response has the content "" And the returned data is false + Scenario: Listing of zero versions + Given I have a "NativeCurlClient" client + And I create a project with name "Test Project" and identifier "test-project" + When I list all versions for project identifier "test-project" + Then the response has the status code "200" + And the response has the content type "application/json" + And the returned data has only the following properties + """ + versions + total_count + """ + And the returned data contains the following data + | property | value | + | versions | [] | + | total_count | 0 | + + Scenario: Listing of multiple versions + Given I have a "NativeCurlClient" client + And I create a project with name "Test Project" and identifier "test-project" + And I create a version with name "Test-Version B" and project identifier "test-project" + And I create a version with name "Test-Version A" and project identifier "test-project" + When I list all versions for project identifier "test-project" + Then the response has the status code "200" + And the response has the content type "application/json" + And the returned data has only the following properties + """ + versions + total_count + """ + And the returned data contains the following data + | property | value | + | total_count | 2 | + And the returned data "versions" property is an array + And the returned data "versions" property contains "2" items + And the returned data "versions.0" property is an array + And the returned data "versions.0" property has only the following properties + """ + id + project + name + description + status + due_date + sharing + wiki_page_title + created_on + updated_on + """ + And the returned data "versions.0" property contains the following data + | property | value | + | id | 1 | + | name | Test-Version B | + | description | | + | status | open | + | sharing | none | + | wiki_page_title | null | + And the returned data "versions.0.project" property contains the following data + | property | value | + | id | 1 | + | name | Test Project | + + Scenario: Updating a version + Given I have a "NativeCurlClient" client + And I create a project with name "Test Project" and identifier "test-project" + And I create a version with project identifier "test-project" with the following data + | property | value | + | name | Test-Version | + When I update the version with id "1" and the following data + | property | value | + | name | New Version name | + Then the response has the status code "204" + And the response has an empty content type + And the response has the content "" + And the returned data is exactly "" + Scenario: Removing a version Given I have a "NativeCurlClient" client And I create a project with name "Test Project" and identifier "test-project" From a95278bd7082f60feeeec7986755f460024cce89 Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 8 Jul 2024 13:45:09 +0200 Subject: [PATCH 6/7] Add Version::listNamesByProject() --- CHANGELOG.md | 1 + src/Redmine/Api/Version.php | 37 +++++ tests/Behat/Bootstrap/VersionContextTrait.php | 14 ++ tests/Behat/features/version.feature | 18 +++ .../Api/Version/ListNamesByProjectTest.php | 128 ++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 tests/Unit/Api/Version/ListNamesByProjectTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab50f15..18663953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New method `Redmine\Api\Role::listNames()` for listing the ids and names of all roles. - New method `Redmine\Api\TimeEntryActivity::listNames()` for listing the ids and names of all time entry activities. - New method `Redmine\Api\Tracker::listNames()` for listing the ids and names of all trackers. +- New method `Redmine\Api\Version::listNamesByProject()` for listing the ids and names of all versions of a project. ### Deprecated diff --git a/src/Redmine/Api/Version.php b/src/Redmine/Api/Version.php index 518d3cb0..e271c66a 100644 --- a/src/Redmine/Api/Version.php +++ b/src/Redmine/Api/Version.php @@ -23,6 +23,8 @@ class Version extends AbstractApi { private $versions = []; + private $versionNames = []; + /** * List versions of a project. * @@ -51,6 +53,41 @@ final public function listByProject($projectIdentifier, array $params = []): arr } } + /** + * Returns an array of all versions by a project with id/name pairs. + * + * @param string|int $projectIdentifier project id or literal identifier + * + * @throws InvalidParameterException if $projectIdentifier is not of type int or string + * + * @return array list of version names (id => name) + */ + final public function listNamesByProject($projectIdentifier): array + { + if (! is_int($projectIdentifier) && ! is_string($projectIdentifier)) { + throw new InvalidParameterException(sprintf( + '%s(): Argument #1 ($projectIdentifier) must be of type int or string', + __METHOD__, + )); + } + + if (array_key_exists($projectIdentifier, $this->versionNames)) { + return $this->versionNames[$projectIdentifier]; + } + + $this->versionNames[$projectIdentifier] = []; + + $list = $this->listByProject($projectIdentifier); + + if (array_key_exists('versions', $list)) { + foreach ($list['versions'] as $version) { + $this->versionNames[$projectIdentifier][(int) $version['id']] = $version['name']; + } + } + + return $this->versionNames[$projectIdentifier]; + } + /** * List versions. * diff --git a/tests/Behat/Bootstrap/VersionContextTrait.php b/tests/Behat/Bootstrap/VersionContextTrait.php index b8798721..250641a6 100644 --- a/tests/Behat/Bootstrap/VersionContextTrait.php +++ b/tests/Behat/Bootstrap/VersionContextTrait.php @@ -72,6 +72,20 @@ public function iListAllVersionsForProjectIdentifier($identifier) ); } + /** + * @When I list all version names for project identifier :identifier + */ + public function iListAllVersionNamesForProjectIdentifier($identifier) + { + /** @var Version */ + $api = $this->getNativeCurlClient()->getApi('version'); + + $this->registerClientResponse( + $api->listNamesByProject($identifier), + $api->getLastResponse(), + ); + } + /** * @When I update the version with id :id and the following data */ diff --git a/tests/Behat/features/version.feature b/tests/Behat/features/version.feature index 3a324f6e..263fd7e0 100644 --- a/tests/Behat/features/version.feature +++ b/tests/Behat/features/version.feature @@ -162,6 +162,24 @@ Feature: Interacting with the REST API for versions | id | 1 | | name | Test Project | + @wip + Scenario: Listing of multiple version names + Given I have a "NativeCurlClient" client + And I create a project with name "Test Project 1" and identifier "test-project-1" + And I create a project with name "Test Project 2" and identifier "test-project-2" + And I create a version with name "Test-Version 1B" and project identifier "test-project-1" + And I create a version with name "Test-Version 1A" and project identifier "test-project-1" + And I create a version with name "Test-Version 2B" and project identifier "test-project-2" + And I create a version with name "Test-Version 2A" and project identifier "test-project-2" + When I list all version names for project identifier "test-project-2" + Then the response has the status code "200" + And the response has the content type "application/json" + And the returned data contains "2" items + And the returned data contains the following data + | property | value | + | 3 | Test-Version 2B | + | 4 | Test-Version 2A | + Scenario: Updating a version Given I have a "NativeCurlClient" client And I create a project with name "Test Project" and identifier "test-project" diff --git a/tests/Unit/Api/Version/ListNamesByProjectTest.php b/tests/Unit/Api/Version/ListNamesByProjectTest.php new file mode 100644 index 00000000..65c3b8a0 --- /dev/null +++ b/tests/Unit/Api/Version/ListNamesByProjectTest.php @@ -0,0 +1,128 @@ +assertSame($expectedResponse, $api->listNamesByProject($projectIdentifier)); + } + + public static function getListNamesByProjectData(): array + { + return [ + 'test without versions' => [ + 5, + '/projects/5/versions.json', + 201, + << [ + 'test-project', + '/projects/test-project/versions.json', + 201, + << "Version 3", + 8 => "Version 2", + 9 => "Version 1", + ], + ], + ]; + } + + public function testListNamesByProjectCallsHttpClientOnlyOnce() + { + $client = AssertingHttpClient::create( + $this, + [ + 'GET', + '/projects/5/versions.json', + 'application/json', + '', + 200, + 'application/json', + <<assertSame([1 => 'Version 1'], $api->listNamesByProject(5)); + $this->assertSame([1 => 'Version 1'], $api->listNamesByProject(5)); + $this->assertSame([1 => 'Version 1'], $api->listNamesByProject(5)); + } + + /** + * @dataProvider Redmine\Tests\Fixtures\TestDataProvider::getInvalidProjectIdentifiers + */ + #[DataProviderExternal(TestDataProvider::class, 'getInvalidProjectIdentifiers')] + public function testListNamesByProjectWithWrongProjectIdentifierThrowsException($projectIdentifier) + { + $api = new Version($this->createMock(HttpClient::class)); + + $this->expectException(InvalidParameterException::class); + $this->expectExceptionMessage('Redmine\Api\Version::listNamesByProject(): Argument #1 ($projectIdentifier) must be of type int or string'); + + $api->listNamesByProject($projectIdentifier); + } +} From 32fd0999f886183c8f616fdc8094ddb4f9fba5a5 Mon Sep 17 00:00:00 2001 From: Art4 Date: Mon, 8 Jul 2024 13:50:24 +0200 Subject: [PATCH 7/7] Deprecate Version::listing() --- CHANGELOG.md | 1 + src/Redmine/Api/Version.php | 5 +++++ tests/Unit/Api/VersionTest.php | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18663953..b63aca36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Redmine\Api\Role::listing()` is deprecated, use `\Redmine\Api\Role::listNames()` instead. - `Redmine\Api\TimeEntryActivity::listing()` is deprecated, use `\Redmine\Api\TimeEntryActivity::listNames()` instead. - `Redmine\Api\Tracker::listing()` is deprecated, use `\Redmine\Api\Tracker::listNames()` instead. +- `Redmine\Api\Version::listing()` is deprecated, use `\Redmine\Api\Version::listNamesByProject()` instead. ## [v2.6.0](https://github.com/kbsali/php-redmine-api/compare/v2.5.0...v2.6.0) - 2024-03-25 diff --git a/src/Redmine/Api/Version.php b/src/Redmine/Api/Version.php index e271c66a..a7fd51ab 100644 --- a/src/Redmine/Api/Version.php +++ b/src/Redmine/Api/Version.php @@ -125,6 +125,9 @@ public function all($project, array $params = []) /** * Returns an array of name/id pairs (or id/name if not $reverse) of versions for $project. * + * @deprecated v2.7.0 Use listNamesByProject() instead. + * @see Version::listNamesByProject() + * * @param string|int $project project id or literal identifier * @param bool $forceUpdate to force the update of the projects var * @param bool $reverse to return an array indexed by name rather than id @@ -134,6 +137,8 @@ public function all($project, array $params = []) */ public function listing($project, $forceUpdate = false, $reverse = true, array $params = []) { + @trigger_error('`' . __METHOD__ . '()` is deprecated since v2.7.0, use `' . __CLASS__ . '::listNamesByProject()` instead.', E_USER_DEPRECATED); + if (true === $forceUpdate || empty($this->versions)) { $this->versions = $this->listByProject($project, $params); } diff --git a/tests/Unit/Api/VersionTest.php b/tests/Unit/Api/VersionTest.php index 6bbbfa7c..0ed55c06 100644 --- a/tests/Unit/Api/VersionTest.php +++ b/tests/Unit/Api/VersionTest.php @@ -246,6 +246,38 @@ public function testListingCallsGetEveryTimeWithForceUpdate() $this->assertSame($expectedReturn, $api->listing(5, true)); } + /** + * Test listing(). + */ + public function testListingTriggersDeprecationWarning() + { + $client = $this->createMock(Client::class); + $client->method('requestGet') + ->willReturn(true); + $client->method('getLastResponseBody') + ->willReturn('{"versions":[{"id":1,"name":"Version 1"},{"id":5,"name":"Version 5"}]}'); + $client->method('getLastResponseContentType') + ->willReturn('application/json'); + + $api = new Version($client); + + // PHPUnit 10 compatible way to test trigger_error(). + set_error_handler( + function ($errno, $errstr): bool { + $this->assertSame( + '`Redmine\Api\Version::listing()` is deprecated since v2.7.0, use `Redmine\Api\Version::listNamesByProject()` instead.', + $errstr, + ); + + restore_error_handler(); + return true; + }, + E_USER_DEPRECATED, + ); + + $api->listing(5); + } + /** * Test getIdByName(). */ @@ -282,7 +314,6 @@ public function testGetIdByNameMakesGetRequest() * * @dataProvider invalidSharingProvider * - * * @param string $sharingValue */ #[DataProvider('invalidSharingProvider')]