diff --git a/CHANGELOG.md b/CHANGELOG.md index 016b9651..14d56264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- New method `Redmine\Api\CustomField::listNames()` for listing the ids and names of all custom fields. - 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\CustomField::listing()` is deprecated, use `\Redmine\Api\CustomField::listNames()` instead. - `Redmine\Api\Group::listing()` is deprecated, use `\Redmine\Api\Group::listNames()` instead. - `Redmine\Api\IssueCategory::listing()` is deprecated, use `\Redmine\Api\IssueCategory::listNamesByProject()` instead. diff --git a/src/Redmine/Api/CustomField.php b/src/Redmine/Api/CustomField.php index 90d40313..54c1366b 100644 --- a/src/Redmine/Api/CustomField.php +++ b/src/Redmine/Api/CustomField.php @@ -17,6 +17,8 @@ class CustomField extends AbstractApi { private $customFields = []; + private $customFieldNames = null; + /** * List custom fields. * @@ -37,6 +39,30 @@ final public function list(array $params = []): array } } + /** + * Returns an array of all custom fields with id/name pairs. + * + * @return array list of custom fields (id => name) + */ + final public function listNames(): array + { + if ($this->customFieldNames !== null) { + return $this->customFieldNames; + } + + $this->customFieldNames = []; + + $list = $this->list(); + + if (array_key_exists('custom_fields', $list)) { + foreach ($list['custom_fields'] as $customField) { + $this->customFieldNames[(int) $customField['id']] = (string) $customField['name']; + } + } + + return $this->customFieldNames; + } + /** * List custom fields. * @@ -73,6 +99,9 @@ public function all(array $params = []) /** * Returns an array of custom fields with name/id pairs. * + * @deprecated v2.7.0 Use listNames() instead. + * @see CustomField::listNames() + * * @param bool $forceUpdate to force the update of the custom fields var * @param array $params optional parameters to be passed to the api (offset, limit, ...) * @@ -80,6 +109,8 @@ public function all(array $params = []) */ public function listing($forceUpdate = false, array $params = []) { + @trigger_error('`' . __METHOD__ . '()` is deprecated since v2.7.0, use `' . __CLASS__ . '::listNames()` instead.', E_USER_DEPRECATED); + if (empty($this->customFields) || $forceUpdate) { $this->customFields = $this->list($params); } diff --git a/tests/Behat/Bootstrap/CustomFieldContextTrait.php b/tests/Behat/Bootstrap/CustomFieldContextTrait.php new file mode 100644 index 00000000..4c20ea33 --- /dev/null +++ b/tests/Behat/Bootstrap/CustomFieldContextTrait.php @@ -0,0 +1,74 @@ +redmine->excecuteDatabaseQuery( + 'INSERT INTO custom_fields(type, name, field_format, is_required, is_for_all, position) VALUES(:type, :name, :field_format, :is_required, :is_for_all, :position);', + [], + [ + ':type' => 'IssueCustomField', + ':name' => $customFieldName, + ':field_format' => 'string', + ':is_required' => 0, + ':is_for_all' => 1, + ':position' => 1, + ], + ); + } + + /** + * @Given I enable the tracker with ID :trackerId for custom field with ID :customFieldId + */ + public function iEnableTheTrackerWithIdForCustomFieldWithId($trackerId, $customFieldId) + { + // support for enabling custom fields for trackers via REST API is missing + $this->redmine->excecuteDatabaseQuery( + 'INSERT INTO custom_fields_trackers(custom_field_id, tracker_id) VALUES(:custom_field_id, :tracker_id);', + [], + [ + ':custom_field_id' => $customFieldId, + ':tracker_id' => $trackerId, + ], + ); + } + + /** + * @When I list all custom fields + */ + public function iListAllCustomFields() + { + /** @var CustomField */ + $api = $this->getNativeCurlClient()->getApi('custom_fields'); + + $this->registerClientResponse( + $api->list(), + $api->getLastResponse(), + ); + } + + /** + * @When I list all custom field names + */ + public function iListAllCustomFieldNames() + { + /** @var CustomField */ + $api = $this->getNativeCurlClient()->getApi('custom_fields'); + + $this->registerClientResponse( + $api->listNames(), + $api->getLastResponse(), + ); + } +} diff --git a/tests/Behat/Bootstrap/FeatureContext.php b/tests/Behat/Bootstrap/FeatureContext.php index 9de9dc61..0c2d71d2 100644 --- a/tests/Behat/Bootstrap/FeatureContext.php +++ b/tests/Behat/Bootstrap/FeatureContext.php @@ -23,6 +23,7 @@ final class FeatureContext extends TestCase implements Context { use AttachmentContextTrait; + use CustomFieldContextTrait; use GroupContextTrait; use IssueCategoryContextTrait; use IssueContextTrait; diff --git a/tests/Behat/Bootstrap/IssueContextTrait.php b/tests/Behat/Bootstrap/IssueContextTrait.php index 4a2e146b..39818ede 100644 --- a/tests/Behat/Bootstrap/IssueContextTrait.php +++ b/tests/Behat/Bootstrap/IssueContextTrait.php @@ -15,11 +15,7 @@ trait IssueContextTrait */ public function iCreateAnIssueWithTheFollowingData(TableNode $table) { - $data = []; - - foreach ($table as $row) { - $data[$row['property']] = $row['value']; - } + $data = $this->prepareIssueData($table); /** @var Issue */ $api = $this->getNativeCurlClient()->getApi('issue'); @@ -35,11 +31,7 @@ public function iCreateAnIssueWithTheFollowingData(TableNode $table) */ public function iUpdateTheIssueWithIdAndTheFollowingData($issueId, TableNode $table) { - $data = []; - - foreach ($table as $row) { - $data[$row['property']] = $row['value']; - } + $data = $this->prepareIssueData($table); /** @var Issue */ $api = $this->getNativeCurlClient()->getApi('issue'); @@ -105,4 +97,23 @@ public function iRemoveTheIssueWithId($issueId) $api->getLastResponse(), ); } + + private function prepareIssueData(TableNode $table): array + { + $data = []; + + foreach ($table as $row) { + $key = $row['property']; + $value = $row['value']; + + // Support for json in custom_fields + if ($key === 'custom_fields') { + $value = json_decode($value, true); + } + + $data[$key] = $value; + } + + return $data; + } } diff --git a/tests/Behat/features/custom_field.feature b/tests/Behat/features/custom_field.feature new file mode 100644 index 00000000..871d1d7f --- /dev/null +++ b/tests/Behat/features/custom_field.feature @@ -0,0 +1,117 @@ +@custom_field +Feature: Interacting with the REST API for custom fields + In order to interact with REST API for custom fields + As a user + I want to make sure the Redmine server replies with the correct response + + Scenario: Listing of zero custom fields + Given I have a "NativeCurlClient" client + When I list all custom fields + 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 + """ + custom_fields + """ + And the returned data "custom_fields" property is an array + And the returned data "custom_fields" property contains "0" items + + Scenario: Listing of multiple custom fields + Given I have a "NativeCurlClient" client + And I create a custom field for issues with the name "Note B" + And I create a custom field for issues with the name "Note A" + When I list all custom fields + 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 + """ + custom_fields + """ + And the returned data "custom_fields" property is an array + And the returned data "custom_fields" property contains "2" items + # field 'description' was added in Redmine 5.1.0, see https://www.redmine.org/issues/37617 + And the returned data "custom_fields.0" property contains the following data with Redmine version ">= 5.1.0" + | property | value | + | id | 1 | + | name | Note B | + | description | null | + | customized_type | issue | + | field_format | string | + | regexp | | + | min_length | null | + | max_length | null | + | is_required | false | + | is_filter | false | + | searchable | false | + | multiple | false | + | default_value | null | + | visible | true | + | trackers | [] | + | roles | [] | + But the returned data "custom_fields.0" property contains the following data with Redmine version "< 5.1.0" + | property | value | + | id | 1 | + | name | Note B | + | customized_type | issue | + | field_format | string | + | regexp | | + | min_length | null | + | max_length | null | + | is_required | false | + | is_filter | false | + | searchable | false | + | multiple | false | + | default_value | null | + | visible | true | + | trackers | [] | + | roles | [] | + # field 'description' was added in Redmine 5.1.0, see https://www.redmine.org/issues/37617 + And the returned data "custom_fields.1" property contains the following data with Redmine version ">= 5.1.0" + | property | value | + | id | 2 | + | name | Note A | + | description | null | + | customized_type | issue | + | field_format | string | + | regexp | | + | min_length | null | + | max_length | null | + | is_required | false | + | is_filter | false | + | searchable | false | + | multiple | false | + | default_value | null | + | visible | true | + | trackers | [] | + | roles | [] | + But the returned data "custom_fields.1" property contains the following data with Redmine version "< 5.1.0" + | property | value | + | id | 2 | + | name | Note A | + | customized_type | issue | + | field_format | string | + | regexp | | + | min_length | null | + | max_length | null | + | is_required | false | + | is_filter | false | + | searchable | false | + | multiple | false | + | default_value | null | + | visible | true | + | trackers | [] | + | roles | [] | + + Scenario: Listing of multiple custom field names + Given I have a "NativeCurlClient" client + And I create a custom field for issues with the name "Note B" + And I create a custom field for issues with the name "Note A" + When I list all custom field names + Then the response has the status code "200" + And the response has the content type "application/json" + And the returned data is an array + And the returned data contains "2" items + And the returned data contains the following data + | property | value | + | 1 | Note B | + | 2 | Note A | diff --git a/tests/Behat/features/issue.feature b/tests/Behat/features/issue.feature index 842fc576..a8932171 100644 --- a/tests/Behat/features/issue.feature +++ b/tests/Behat/features/issue.feature @@ -122,6 +122,70 @@ Feature: Interacting with the REST API for issues | id | 1 | | name | Redmine Admin | + @custom_field + Scenario: Creating an issue with custom field + Given I have a "NativeCurlClient" client + And I have an issue status with the name "New" + And I have an issue priority with the name "Normal" + And I have a tracker with the name "Defect" and default status id "1" + And I create a project with name "Test Project" and identifier "test-project" + And I create a custom field for issues with the name "Note" + And I enable the tracker with ID "1" for custom field with ID "1" + When I create an issue with the following data + | property | value | + | subject | issue subject | + | project | Test Project | + | tracker | Defect | + | priority | Normal | + | status | New | + | custom_fields | [{"id":1,"value":"Note for custom field"}] | + Then the response has the status code "201" + And the response has the content type "application/xml" + And the returned data is an instance of "SimpleXMLElement" + And the returned data has only the following properties + """ + id + project + tracker + status + priority + author + subject + description + start_date + due_date + done_ratio + is_private + estimated_hours + total_estimated_hours + custom_fields + created_on + updated_on + closed_on + """ + And the returned data has proterties with the following data + | property | value | + | id | 1 | + | subject | issue subject | + | description | [] | + | due_date | [] | + | done_ratio | 0 | + | is_private | false | + | estimated_hours | [] | + | total_estimated_hours | [] | + And the returned data "custom_fields.custom_field" property has only the following properties + """ + @attributes + value + """ + And the returned data "custom_fields.custom_field.@attributes" property contains the following data + | property | value | + | id | 1 | + | name | Note | + And the returned data "custom_fields.custom_field" property contains the following data + | property | value | + | value | Note for custom field | + Scenario: Updating an issue Given I have a "NativeCurlClient" client And I have an issue status with the name "New" diff --git a/tests/Unit/Api/CustomField/ListNamesTest.php b/tests/Unit/Api/CustomField/ListNamesTest.php new file mode 100644 index 00000000..a79e17c8 --- /dev/null +++ b/tests/Unit/Api/CustomField/ListNamesTest.php @@ -0,0 +1,108 @@ +assertSame($expectedResponse, $api->listNames()); + } + + public static function getListNamesData(): array + { + return [ + 'test without custom fields' => [ + '/custom_fields.json', + 201, + << [ + '/custom_fields.json', + 201, + << "CustomField 3", + 8 => "CustomField 2", + 9 => "CustomField 1", + ], + ], + ]; + } + + public function testListNamesCallsHttpClientOnlyOnce() + { + $client = AssertingHttpClient::create( + $this, + [ + 'GET', + '/custom_fields.json', + 'application/json', + '', + 200, + 'application/json', + <<assertSame([1 => 'CustomField 1'], $api->listNames()); + $this->assertSame([1 => 'CustomField 1'], $api->listNames()); + $this->assertSame([1 => 'CustomField 1'], $api->listNames()); + } +} diff --git a/tests/Unit/Api/CustomFieldTest.php b/tests/Unit/Api/CustomFieldTest.php index b360c0cb..e6a152a8 100644 --- a/tests/Unit/Api/CustomFieldTest.php +++ b/tests/Unit/Api/CustomFieldTest.php @@ -287,6 +287,38 @@ public function testListingCallsGetEveryTimeWithForceUpdate() $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('{"custom_fields":[{"id":1,"name":"CustomField 1"},{"id":5,"name":"CustomField 5"}]}'); + $client->method('getLastResponseContentType') + ->willReturn('application/json'); + + $api = new CustomField($client); + + // PHPUnit 10 compatible way to test trigger_error(). + set_error_handler( + function ($errno, $errstr): bool { + $this->assertSame( + '`Redmine\Api\CustomField::listing()` is deprecated since v2.7.0, use `Redmine\Api\CustomField::listNames()` instead.', + $errstr, + ); + + restore_error_handler(); + return true; + }, + E_USER_DEPRECATED, + ); + + $api->listing(); + } + /** * Test getIdByName(). */