diff --git a/CHANGELOG.md b/CHANGELOG.md index ab9803b7..016b9651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - New method `Redmine\Api\Group::listNames()` for listing the ids and names of all groups. +- New method `Redmine\Api\IssueCategory::listNamesByProject()` for listing the ids and names of all issue categories of a project. ### Deprecated - `Redmine\Api\Group::listing()` is deprecated, use `\Redmine\Api\Group::listNames()` instead. +- `Redmine\Api\IssueCategory::listing()` is deprecated, use `\Redmine\Api\IssueCategory::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/IssueCategory.php b/src/Redmine/Api/IssueCategory.php index 864040d8..25a05eb9 100644 --- a/src/Redmine/Api/IssueCategory.php +++ b/src/Redmine/Api/IssueCategory.php @@ -24,6 +24,8 @@ class IssueCategory extends AbstractApi { private $issueCategories = []; + private $issueCategoriesNames = []; + /** * List issue categories for a given project. * @@ -53,6 +55,41 @@ final public function listByProject($projectIdentifier, array $params = []): arr } } + /** + * Returns an array of all issue categories 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 issue category 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->issueCategoriesNames)) { + return $this->issueCategoriesNames[$projectIdentifier]; + } + + $this->issueCategoriesNames[$projectIdentifier] = []; + + $list = $this->listByProject($projectIdentifier); + + if (array_key_exists('issue_categories', $list)) { + foreach ($list['issue_categories'] as $category) { + $this->issueCategoriesNames[$projectIdentifier][(int) $category['id']] = $category['name']; + } + } + + return $this->issueCategoriesNames[$projectIdentifier]; + } + /** * List issue categories. * @@ -88,6 +125,9 @@ public function all($project, array $params = []) /** * Returns an array of categories with name/id pairs. * + * @deprecated v2.7.0 Use listNamesByProject() instead. + * @see IssueCategory::listNamesByProject() + * * @param string|int $project project id or literal identifier * @param bool $forceUpdate to force the update of the projects var * @@ -95,6 +135,8 @@ public function all($project, array $params = []) */ public function listing($project, $forceUpdate = false) { + @trigger_error('`' . __METHOD__ . '()` is deprecated since v2.7.0, use `' . __CLASS__ . '::listNamesByProject()` instead.', E_USER_DEPRECATED); + if (true === $forceUpdate || empty($this->issueCategories)) { $this->issueCategories = $this->listByProject($project); } diff --git a/tests/Behat/Bootstrap/IssueCategoryContextTrait.php b/tests/Behat/Bootstrap/IssueCategoryContextTrait.php index 7352e1d8..36dfd24c 100644 --- a/tests/Behat/Bootstrap/IssueCategoryContextTrait.php +++ b/tests/Behat/Bootstrap/IssueCategoryContextTrait.php @@ -29,6 +29,34 @@ public function iCreateAnIssueCategoryForProjectIdentifierAndWithTheFollowingDat ); } + /** + * @When I list all issue categories for project identifier :identifier + */ + public function iListAllIssueCategoriesForProjectIdentifier($identifier) + { + /** @var IssueCategory */ + $api = $this->getNativeCurlClient()->getApi('issue_category'); + + $this->registerClientResponse( + $api->listByProject($identifier), + $api->getLastResponse(), + ); + } + + /** + * @When I list all issue category names for project identifier :identifier + */ + public function iListAllIssueCategoryNamesForProjectIdentifier($identifier) + { + /** @var IssueCategory */ + $api = $this->getNativeCurlClient()->getApi('issue_category'); + + $this->registerClientResponse( + $api->listNamesByProject($identifier), + $api->getLastResponse(), + ); + } + /** * @When I update the issue category with id :id and the following data */ diff --git a/tests/Behat/features/groups.feature b/tests/Behat/features/groups.feature index a1971547..fbe9cd34 100644 --- a/tests/Behat/features/groups.feature +++ b/tests/Behat/features/groups.feature @@ -57,11 +57,11 @@ Feature: Interacting with the REST API for groups Scenario: Listing names of all groups Given I have a "NativeCurlClient" client - And I create a group with name "Test Group 1" - And I create a group with name "Test Group 2" - And I create a group with name "Test Group 3" - And I create a group with name "Test Group 4" - And I create a group with name "Test Group 5" + And I create a group with name "Test Group D" + And I create a group with name "Test Group E" + And I create a group with name "Test Group C" + And I create a group with name "Test Group B" + And I create a group with name "Test Group A" When I list the names of all groups Then the response has the status code "200" And the response has the content type "application/json" @@ -69,11 +69,11 @@ Feature: Interacting with the REST API for groups And the returned data contains "5" items And the returned data contains the following data | property | value | - | 4 | Test Group 1 | - | 5 | Test Group 2 | - | 6 | Test Group 3 | - | 7 | Test Group 4 | - | 8 | Test Group 5 | + | 4 | Test Group D | + | 5 | Test Group E | + | 6 | Test Group C | + | 7 | Test Group B | + | 8 | Test Group A | Scenario: Showing a specific group Given I have a "NativeCurlClient" client diff --git a/tests/Behat/features/issue_category.feature b/tests/Behat/features/issue_category.feature index 1714c348..33a6cef8 100644 --- a/tests/Behat/features/issue_category.feature +++ b/tests/Behat/features/issue_category.feature @@ -82,6 +82,92 @@ Feature: Interacting with the REST API for issue categories | id | 1 | | name | Redmine Admin | + Scenario: Listing of zero issue categories + Given I have a "NativeCurlClient" client + And I create a project with name "Test Project" and identifier "test-project" + When I list all issue categories 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 + """ + issue_categories + total_count + """ + And the returned data contains the following data + | property | value | + | issue_categories | [] | + | total_count | 0 | + + Scenario: Listing of multiple issue categories + Given I have a "NativeCurlClient" client + And I create a project with name "Test Project" and identifier "test-project" + And I create an issue category for project identifier "test-project" and with the following data + | property | value | + | name | Category name B | + And I create an issue category for project identifier "test-project" and with the following data + | property | value | + | name | Category name A | + When I list all issue categories 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 + """ + issue_categories + total_count + """ + And the returned data contains the following data + | property | value | + | total_count | 2 | + And the returned data "issue_categories" property is an array + And the returned data "issue_categories" property contains "2" items + And the returned data "issue_categories.0" property is an array + And the returned data "issue_categories.0" property has only the following properties + """ + id + project + name + """ + And the returned data "issue_categories.0" property contains the following data + | property | value | + | id | 2 | + | name | Category name A | + And the returned data "issue_categories.0.project" property contains the following data + | property | value | + | id | 1 | + | name | Test Project | + And the returned data "issue_categories.1" property is an array + And the returned data "issue_categories.1" property has only the following properties + """ + id + project + name + """ + And the returned data "issue_categories.1" property contains the following data + | property | value | + | id | 1 | + | name | Category name B | + And the returned data "issue_categories.1.project" property contains the following data + | property | value | + | id | 1 | + | name | Test Project | + + Scenario: Listing of multiple issue category names + Given I have a "NativeCurlClient" client + And I create a project with name "Test Project" and identifier "test-project" + And I create an issue category for project identifier "test-project" and with the following data + | property | value | + | name | Category name B | + And I create an issue category for project identifier "test-project" and with the following data + | property | value | + | name | Category name A | + When I list all issue category names 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 contains the following data + | property | value | + | 1 | Category name B | + | 2 | Category name A | + Scenario: Updating an issue category with all data Given I have a "NativeCurlClient" client And I create a project with name "Test Project" and identifier "test-project" diff --git a/tests/Fixtures/TestDataProvider.php b/tests/Fixtures/TestDataProvider.php new file mode 100644 index 00000000..3db9b173 --- /dev/null +++ b/tests/Fixtures/TestDataProvider.php @@ -0,0 +1,22 @@ + [null], + 'true' => [true], + 'false' => [false], + 'float' => [0.0], + 'array' => [[]], + 'object' => [new stdClass()], + ]; + } +} diff --git a/tests/Unit/Api/Group/ListNamesTest.php b/tests/Unit/Api/Group/ListNamesTest.php index 39829d98..55c0d990 100644 --- a/tests/Unit/Api/Group/ListNamesTest.php +++ b/tests/Unit/Api/Group/ListNamesTest.php @@ -58,16 +58,16 @@ public static function getListNamesData(): array << "Group 1", - 8 => "Group 2", 7 => "Group 3", + 8 => "Group 2", + 9 => "Group 1", ], ], ]; diff --git a/tests/Unit/Api/IssueCategory/ListByProjectTest.php b/tests/Unit/Api/IssueCategory/ListByProjectTest.php index 52a0b920..a3592ed1 100644 --- a/tests/Unit/Api/IssueCategory/ListByProjectTest.php +++ b/tests/Unit/Api/IssueCategory/ListByProjectTest.php @@ -3,14 +3,14 @@ namespace Redmine\Tests\Unit\Api\IssueCategory; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; use Redmine\Api\IssueCategory; use Redmine\Client\Client; use Redmine\Exception\InvalidParameterException; use Redmine\Exception\UnexpectedResponseException; use Redmine\Tests\Fixtures\MockClient; -use stdClass; +use Redmine\Tests\Fixtures\TestDataProvider; #[CoversClass(IssueCategory::class)] class ListByProjectTest extends TestCase @@ -71,9 +71,9 @@ public function testListByProjectWithParametersReturnsResponse() } /** - * @dataProvider getInvalidProjectIdentifiers + * @dataProvider Redmine\Tests\Fixtures\TestDataProvider::getInvalidProjectIdentifiers */ - #[DataProvider('getInvalidProjectIdentifiers')] + #[DataProviderExternal(TestDataProvider::class, 'getInvalidProjectIdentifiers')] public function testListByProjectWithWrongProjectIdentifierThrowsException($projectIdentifier) { $api = new IssueCategory(MockClient::create()); @@ -84,18 +84,6 @@ public function testListByProjectWithWrongProjectIdentifierThrowsException($proj $api->listByProject($projectIdentifier); } - public static function getInvalidProjectIdentifiers(): array - { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'float' => [0.0], - 'array' => [[]], - 'object' => [new stdClass()], - ]; - } - public function testListByProjectThrowsException() { // Create the used mock objects diff --git a/tests/Unit/Api/IssueCategory/ListNamesByProjectTest.php b/tests/Unit/Api/IssueCategory/ListNamesByProjectTest.php new file mode 100644 index 00000000..3ec124d1 --- /dev/null +++ b/tests/Unit/Api/IssueCategory/ListNamesByProjectTest.php @@ -0,0 +1,128 @@ +assertSame($expectedResponse, $api->listNamesByProject($projectIdentifier)); + } + + public static function getListNamesByProjectData(): array + { + return [ + 'test without issue categories' => [ + 5, + '/projects/5/issue_categories.json', + 201, + << [ + 'test-project', + '/projects/test-project/issue_categories.json', + 201, + << "IssueCategory 3", + 8 => "IssueCategory 2", + 9 => "IssueCategory 1", + ], + ], + ]; + } + + public function testListNamesByProjectCallsHttpClientOnlyOnce() + { + $client = AssertingHttpClient::create( + $this, + [ + 'GET', + '/projects/5/issue_categories.json', + 'application/json', + '', + 200, + 'application/json', + <<assertSame([1 => 'IssueCategory 1'], $api->listNamesByProject(5)); + $this->assertSame([1 => 'IssueCategory 1'], $api->listNamesByProject(5)); + $this->assertSame([1 => 'IssueCategory 1'], $api->listNamesByProject(5)); + } + + /** + * @dataProvider Redmine\Tests\Fixtures\TestDataProvider::getInvalidProjectIdentifiers + */ + #[DataProviderExternal(TestDataProvider::class, 'getInvalidProjectIdentifiers')] + public function testListNamesByProjectWithWrongProjectIdentifierThrowsException($projectIdentifier) + { + $api = new IssueCategory($this->createMock(HttpClient::class)); + + $this->expectException(InvalidParameterException::class); + $this->expectExceptionMessage('Redmine\Api\IssueCategory::listNamesByProject(): Argument #1 ($projectIdentifier) must be of type int or string'); + + $api->listNamesByProject($projectIdentifier); + } +} diff --git a/tests/Unit/Api/IssueCategoryTest.php b/tests/Unit/Api/IssueCategoryTest.php index 26156bae..5c011f2d 100644 --- a/tests/Unit/Api/IssueCategoryTest.php +++ b/tests/Unit/Api/IssueCategoryTest.php @@ -219,6 +219,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('{"issue_categories":[{"id":1,"name":"IssueCategory 1"},{"id":5,"name":"IssueCategory 5"}]}'); + $client->method('getLastResponseContentType') + ->willReturn('application/json'); + + $api = new IssueCategory($client); + + // PHPUnit 10 compatible way to test trigger_error(). + set_error_handler( + function ($errno, $errstr): bool { + $this->assertSame( + '`Redmine\Api\IssueCategory::listing()` is deprecated since v2.7.0, use `Redmine\Api\IssueCategory::listNamesByProject()` instead.', + $errstr, + ); + + restore_error_handler(); + return true; + }, + E_USER_DEPRECATED, + ); + + $api->listing(5); + } + /** * Test getIdByName(). */ diff --git a/tests/Unit/Api/Membership/ListByProjectTest.php b/tests/Unit/Api/Membership/ListByProjectTest.php index c6c4483d..c877e0b0 100644 --- a/tests/Unit/Api/Membership/ListByProjectTest.php +++ b/tests/Unit/Api/Membership/ListByProjectTest.php @@ -3,14 +3,14 @@ namespace Redmine\Tests\Unit\Api\Membership; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; use Redmine\Api\Membership; use Redmine\Client\Client; use Redmine\Exception\InvalidParameterException; use Redmine\Exception\UnexpectedResponseException; use Redmine\Tests\Fixtures\MockClient; -use stdClass; +use Redmine\Tests\Fixtures\TestDataProvider; #[CoversClass(Membership::class)] class ListByProjectTest extends TestCase @@ -69,9 +69,9 @@ public function testListByProjectWithParametersReturnsResponse() } /** - * @dataProvider getInvalidProjectIdentifiers + * @dataProvider Redmine\Tests\Fixtures\TestDataProvider::getInvalidProjectIdentifiers */ - #[DataProvider('getInvalidProjectIdentifiers')] + #[DataProviderExternal(TestDataProvider::class, 'getInvalidProjectIdentifiers')] public function testListByProjectWithWrongProjectIdentifierThrowsException($projectIdentifier) { $api = new Membership(MockClient::create()); @@ -82,18 +82,6 @@ public function testListByProjectWithWrongProjectIdentifierThrowsException($proj $api->listByProject($projectIdentifier); } - public static function getInvalidProjectIdentifiers(): array - { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'float' => [0.0], - 'array' => [[]], - 'object' => [new stdClass()], - ]; - } - public function testListByProjectThrowsException() { // Create the used mock objects diff --git a/tests/Unit/Api/News/ListByProjectTest.php b/tests/Unit/Api/News/ListByProjectTest.php index 4fa4db83..61009a6e 100644 --- a/tests/Unit/Api/News/ListByProjectTest.php +++ b/tests/Unit/Api/News/ListByProjectTest.php @@ -3,14 +3,14 @@ namespace Redmine\Tests\Unit\Api\News; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; use Redmine\Api\News; use Redmine\Client\Client; use Redmine\Exception\InvalidParameterException; use Redmine\Exception\UnexpectedResponseException; use Redmine\Tests\Fixtures\MockClient; -use stdClass; +use Redmine\Tests\Fixtures\TestDataProvider; #[CoversClass(News::class)] class ListByProjectTest extends TestCase @@ -71,9 +71,9 @@ public function testListByProjectWithParametersReturnsResponse() } /** - * @dataProvider getInvalidProjectIdentifiers + * @dataProvider Redmine\Tests\Fixtures\TestDataProvider::getInvalidProjectIdentifiers */ - #[DataProvider('getInvalidProjectIdentifiers')] + #[DataProviderExternal(TestDataProvider::class, 'getInvalidProjectIdentifiers')] public function testListByProjectWithWrongProjectIdentifierThrowsException($projectIdentifier) { $api = new News(MockClient::create()); @@ -84,18 +84,6 @@ public function testListByProjectWithWrongProjectIdentifierThrowsException($proj $api->listByProject($projectIdentifier); } - public static function getInvalidProjectIdentifiers(): array - { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'float' => [0.0], - 'array' => [[]], - 'object' => [new stdClass()], - ]; - } - public function testListByProjectThrowsException() { // Create the used mock objects diff --git a/tests/Unit/Api/Version/ListByProjectTest.php b/tests/Unit/Api/Version/ListByProjectTest.php index d993f65d..7d6e3171 100644 --- a/tests/Unit/Api/Version/ListByProjectTest.php +++ b/tests/Unit/Api/Version/ListByProjectTest.php @@ -3,14 +3,14 @@ namespace Redmine\Tests\Unit\Api\Version; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; use Redmine\Api\Version; use Redmine\Client\Client; use Redmine\Exception\InvalidParameterException; use Redmine\Exception\UnexpectedResponseException; use Redmine\Tests\Fixtures\MockClient; -use stdClass; +use Redmine\Tests\Fixtures\TestDataProvider; #[CoversClass(Version::class)] class ListByProjectTest extends TestCase @@ -72,9 +72,9 @@ public function testListByProjectWithParametersReturnsResponse() } /** - * @dataProvider getInvalidProjectIdentifiers + * @dataProvider Redmine\Tests\Fixtures\TestDataProvider::getInvalidProjectIdentifiers */ - #[DataProvider('getInvalidProjectIdentifiers')] + #[DataProviderExternal(TestDataProvider::class, 'getInvalidProjectIdentifiers')] public function testListByProjectWithWrongProjectIdentifierThrowsException($projectIdentifier) { $api = new Version(MockClient::create()); @@ -85,18 +85,6 @@ public function testListByProjectWithWrongProjectIdentifierThrowsException($proj $api->listByProject($projectIdentifier); } - public static function getInvalidProjectIdentifiers(): array - { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'float' => [0.0], - 'array' => [[]], - 'object' => [new stdClass()], - ]; - } - public function testListByProjectThrowsException() { // Create the used mock objects diff --git a/tests/Unit/Api/Wiki/ListByProjectTest.php b/tests/Unit/Api/Wiki/ListByProjectTest.php index 31381a22..43bd53eb 100644 --- a/tests/Unit/Api/Wiki/ListByProjectTest.php +++ b/tests/Unit/Api/Wiki/ListByProjectTest.php @@ -3,14 +3,14 @@ namespace Redmine\Tests\Unit\Api\Wiki; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\DataProviderExternal; use PHPUnit\Framework\TestCase; use Redmine\Api\Wiki; use Redmine\Client\Client; use Redmine\Exception\InvalidParameterException; use Redmine\Exception\UnexpectedResponseException; use Redmine\Tests\Fixtures\MockClient; -use stdClass; +use Redmine\Tests\Fixtures\TestDataProvider; #[CoversClass(Wiki::class)] class ListByProjectTest extends TestCase @@ -72,9 +72,9 @@ public function testListByProjectWithParametersReturnsResponse() } /** - * @dataProvider getInvalidProjectIdentifiers + * @dataProvider Redmine\Tests\Fixtures\TestDataProvider::getInvalidProjectIdentifiers */ - #[DataProvider('getInvalidProjectIdentifiers')] + #[DataProviderExternal(TestDataProvider::class, 'getInvalidProjectIdentifiers')] public function testListByProjectWithWrongProjectIdentifierThrowsException($projectIdentifier) { $api = new Wiki(MockClient::create()); @@ -85,18 +85,6 @@ public function testListByProjectWithWrongProjectIdentifierThrowsException($proj $api->listByProject($projectIdentifier); } - public static function getInvalidProjectIdentifiers(): array - { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'float' => [0.0], - 'array' => [[]], - 'object' => [new stdClass()], - ]; - } - public function testListByProjectThrowsException() { // Create the used mock objects