diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c8e9e6c..fcde1f9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased](https://github.com/kbsali/php-redmine-api/compare/v2.4.0...v2.x)
+### Added
+
+- Added support for updating groups.
+
### Changed
- The last response is saved in `Redmine\Api\AbstractApi` to prevent race conditions with `Redmine\Client\Client` implementations.
diff --git a/docs/usage.md b/docs/usage.md
index f0c5ac4a..d195eccd 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -489,6 +489,25 @@ $client->getApi('group')->removeUser($groupId, $userId);
$client->getApi('group')->create([
'name' => 'asdf',
'user_ids' => [1, 2],
+ 'custom_fields' => [
+ [
+ 'id' => 123,
+ 'name' => 'cf_name',
+ 'value' => 'cf_value',
+ ],
+ ],
+]);
+$client->getApi('group')->update($groupId, [
+ 'name' => 'asdf',
+ // Note: you can only add users this way; use removeUser to remove a user
+ 'user_ids' => [1, 2],
+ 'custom_fields' => [
+ [
+ 'id' => 123,
+ 'name' => 'cf_name',
+ 'value' => 'cf_value',
+ ],
+ ],
]);
// ----------------------------
diff --git a/src/Redmine/Api/Group.php b/src/Redmine/Api/Group.php
index 206dcc62..4ee6442d 100644
--- a/src/Redmine/Api/Group.php
+++ b/src/Redmine/Api/Group.php
@@ -8,6 +8,7 @@
use Redmine\Exception\UnexpectedResponseException;
use Redmine\Serializer\PathSerializer;
use Redmine\Serializer\XmlSerializer;
+use SimpleXMLElement;
/**
* Handling of groups.
@@ -101,7 +102,7 @@ public function listing($forceUpdate = false)
*
* @throws MissingParameterException Missing mandatory parameters
*
- * @return string|false
+ * @return string|SimpleXMLElement|false
*/
public function create(array $params = [])
{
@@ -124,17 +125,28 @@ public function create(array $params = [])
}
/**
+ * Updates a group.
+ *
* NOT DOCUMENTED in Redmine's wiki.
*
* @see http://www.redmine.org/projects/redmine/wiki/Rest_Groups#PUT
*
- * @param int $id
+ * @param int $id the group id
*
- * @throws Exception Not implemented
+ * @return string empty string
*/
- public function update($id, array $params = [])
+ public function update(int $id, array $params = [])
{
- throw new \Exception('Not implemented');
+ $defaults = [
+ 'name' => null,
+ 'user_ids' => null,
+ ];
+ $params = $this->sanitizeParams($defaults, $params);
+
+ return $this->put(
+ '/groups/' . $id . '.xml',
+ XmlSerializer::createFromArray(['group' => $params])->getEncoded()
+ );
}
/**
diff --git a/tests/Integration/GroupXmlTest.php b/tests/Integration/GroupXmlTest.php
index 3f7af662..6b64ce41 100644
--- a/tests/Integration/GroupXmlTest.php
+++ b/tests/Integration/GroupXmlTest.php
@@ -2,7 +2,6 @@
namespace Redmine\Tests\Integration;
-use Exception;
use PHPUnit\Framework\TestCase;
use Redmine\Exception\MissingParameterException;
use Redmine\Tests\Fixtures\MockClient;
@@ -48,15 +47,30 @@ public function testCreateComplex()
);
}
- public function testUpdateNotImplemented()
+ public function testUpdateComplex()
{
/** @var \Redmine\Api\Group */
$api = MockClient::create()->getApi('group');
- $this->assertInstanceOf('Redmine\Api\Group', $api);
-
- $this->expectException(Exception::class);
- $this->expectExceptionMessage('Not implemented');
+ $res = $api->update(5, [
+ 'name' => 'Developers',
+ 'user_ids' => [3, 5],
+ ]);
+ $response = json_decode($res, true);
- $api->update(1);
+ $this->assertEquals('PUT', $response['method']);
+ $this->assertEquals('/groups/5.xml', $response['path']);
+ $this->assertXmlStringEqualsXmlString(
+ <<< XML
+
+
+ Developers
+
+ 3
+ 5
+
+
+ XML,
+ $response['data']
+ );
}
}
diff --git a/tests/Unit/Api/Group/CreateTest.php b/tests/Unit/Api/Group/CreateTest.php
new file mode 100644
index 00000000..7e34cdce
--- /dev/null
+++ b/tests/Unit/Api/Group/CreateTest.php
@@ -0,0 +1,137 @@
+createMock(HttpClient::class);
+ $client->expects($this->exactly(1))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ $this->assertSame('POST', $method);
+ $this->assertSame('/groups.xml', $path);
+ $this->assertXmlStringEqualsXmlString('Group Name', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ });
+
+ // Create the object under test
+ $api = new Group($client);
+
+ // Perform the tests
+ $xmlElement = $api->create(['name' => 'Group Name']);
+
+ $this->assertInstanceOf(SimpleXMLElement::class, $xmlElement);
+ $this->assertXmlStringEqualsXmlString(
+ '',
+ $xmlElement->asXml(),
+ );
+ }
+
+ public function testCreateWithNameAndUserIdsCreatesGroup()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(1))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ $this->assertSame('POST', $method);
+ $this->assertSame('/groups.xml', $path);
+ $this->assertXmlStringEqualsXmlString('Group Name123', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ });
+
+ // Create the object under test
+ $api = new Group($client);
+
+ // Perform the tests
+ $xmlElement = $api->create(['name' => 'Group Name', 'user_ids' => [1, 2, 3]]);
+
+ $this->assertInstanceOf(SimpleXMLElement::class, $xmlElement);
+ $this->assertXmlStringEqualsXmlString(
+ '',
+ $xmlElement->asXml(),
+ );
+ }
+
+ public function testCreateWithNameAndCustomFieldsCreatesGroup()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(1))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ $this->assertSame('POST', $method);
+ $this->assertSame('/groups.xml', $path);
+ $this->assertXmlStringEqualsXmlString('Group Name5', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ });
+
+ // Create the object under test
+ $api = new Group($client);
+
+ // Perform the tests
+ $xmlElement = $api->create([
+ 'name' => 'Group Name',
+ 'custom_fields' => [
+ ['id' => 1, 'value' => 5],
+ ],
+ ]);
+
+ $this->assertInstanceOf(SimpleXMLElement::class, $xmlElement);
+ $this->assertXmlStringEqualsXmlString(
+ '',
+ $xmlElement->asXml(),
+ );
+ }
+
+ public function testCreateThrowsExceptionIfNameIsMissing()
+ {
+ // Test values
+ $postParameter = [];
+
+ // Create the used mock objects
+ $client = $this->createMock(HttpClient::class);
+
+ // Create the object under test
+ $api = new Group($client);
+
+ $this->expectException(MissingParameterException::class);
+ $this->expectExceptionMessage('Theses parameters are mandatory: `name`');
+
+ // Perform the tests
+ $api->create($postParameter);
+ }
+}
diff --git a/tests/Unit/Api/Group/UpdateTest.php b/tests/Unit/Api/Group/UpdateTest.php
new file mode 100644
index 00000000..08bdaed6
--- /dev/null
+++ b/tests/Unit/Api/Group/UpdateTest.php
@@ -0,0 +1,105 @@
+createMock(HttpClient::class);
+ $client->expects($this->exactly(1))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ $this->assertSame('PUT', $method);
+ $this->assertSame('/groups/1.xml', $path);
+ $this->assertXmlStringEqualsXmlString('Group Name', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ });
+
+ // Create the object under test
+ $api = new Group($client);
+
+ // Perform the tests
+ $return = $api->update(1, ['name' => 'Group Name']);
+
+ $this->assertSame('', $return);
+ }
+
+ public function testUpdateWithUserIdsUpdatesGroup()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(1))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ $this->assertSame('PUT', $method);
+ $this->assertSame('/groups/1.xml', $path);
+ $this->assertXmlStringEqualsXmlString('123', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ });
+
+ // Create the object under test
+ $api = new Group($client);
+
+ // Perform the tests
+ $return = $api->update(1, ['user_ids' => [1, 2, 3]]);
+
+ $this->assertSame('', $return);
+ }
+
+ public function testUpdateWithCustomFieldsUpdatesGroup()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(1))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ $this->assertSame('PUT', $method);
+ $this->assertSame('/groups/1.xml', $path);
+ $this->assertXmlStringEqualsXmlString('5', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ });
+
+ // Create the object under test
+ $api = new Group($client);
+
+ // Perform the tests
+ $return = $api->update(1, [
+ 'custom_fields' => [
+ ['id' => 1, 'value' => 5],
+ ],
+ ]);
+
+ $this->assertSame('', $return);
+ }
+}
diff --git a/tests/Unit/Api/GroupTest.php b/tests/Unit/Api/GroupTest.php
index fa5fc56a..8188a499 100644
--- a/tests/Unit/Api/GroupTest.php
+++ b/tests/Unit/Api/GroupTest.php
@@ -1,11 +1,12 @@
assertSame($expectedReturn, $api->remove(5));
}
- /**
- * Test create().
- *
- * @covers ::create
- * @covers ::post
- * @test
- */
- public function testCreateCallsPost()
- {
- // Test values
- $response = 'API Response';
- $postParameter = [
- 'name' => 'Group Name',
- ];
-
- // Create the used mock objects
- $client = $this->createMock(Client::class);
- $client->expects($this->once())
- ->method('requestPost')
- ->with(
- $this->logicalAnd(
- $this->stringStartsWith('/groups'),
- $this->logicalXor(
- $this->stringEndsWith('.json'),
- $this->stringEndsWith('.xml')
- )
- ),
- $this->stringContains('Group Name')
- )
- ->willReturn(true);
- $client->expects($this->exactly(1))
- ->method('getLastResponseBody')
- ->willReturn($response);
-
- // Create the object under test
- $api = new Group($client);
-
- // Perform the tests
- $this->assertSame($response, $api->create($postParameter));
- }
-
- /**
- * Test create().
- *
- * @covers ::create
- * @covers ::post
- *
- * @test
- */
- public function testCreateThrowsExceptionIfNameIsMissing()
- {
- // Test values
- $postParameter = [];
-
- // Create the used mock objects
- $client = $this->createMock(Client::class);
-
- // Create the object under test
- $api = new Group($client);
-
- $this->expectException(MissingParameterException::class);
- $this->expectExceptionMessage('Theses parameters are mandatory: `name`');
-
- // Perform the tests
- $api->create($postParameter);
- }
-
/**
* Test removeUser().
*