diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c845f8e..6c8e9e6c 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)
+### Changed
+
+- The last response is saved in `Redmine\Api\AbstractApi` to prevent race conditions with `Redmine\Client\Client` implementations.
+
## [v2.4.0](https://github.com/kbsali/php-redmine-api/compare/v2.3.0...v2.4.0) - 2024-01-04
### Added
diff --git a/composer.json b/composer.json
index 59982d2f..f9a45932 100644
--- a/composer.json
+++ b/composer.json
@@ -49,6 +49,11 @@
"codestyle": "php-cs-fixer fix",
"coverage": "phpunit --coverage-html=\".phpunit.cache/code-coverage\"",
"phpstan": "phpstan analyze --memory-limit 512M --configuration .phpstan.neon",
- "test": "phpunit"
+ "phpunit": "phpunit",
+ "test": [
+ "@phpstan",
+ "@phpunit",
+ "@codestyle --dry-run --diff"
+ ]
}
}
diff --git a/src/Redmine/Api/AbstractApi.php b/src/Redmine/Api/AbstractApi.php
index 281a90f7..3b2851c2 100644
--- a/src/Redmine/Api/AbstractApi.php
+++ b/src/Redmine/Api/AbstractApi.php
@@ -2,10 +2,14 @@
namespace Redmine\Api;
+use Closure;
+use InvalidArgumentException;
use Redmine\Api;
use Redmine\Client\Client;
use Redmine\Exception;
use Redmine\Exception\SerializerException;
+use Redmine\Http\HttpClient;
+use Redmine\Http\Response;
use Redmine\Serializer\JsonSerializer;
use Redmine\Serializer\PathSerializer;
use Redmine\Serializer\XmlSerializer;
@@ -23,9 +27,52 @@ abstract class AbstractApi implements Api
*/
protected $client;
- public function __construct(Client $client)
+ /**
+ * @var HttpClient
+ */
+ private $httpClient;
+
+ /**
+ * @var Response
+ */
+ private $lastResponse;
+
+ /**
+ * @param Client|HttpClient $client
+ */
+ public function __construct($client)
{
- $this->client = $client;
+ if (! is_object($client) || (! $client instanceof Client && ! $client instanceof HttpClient)) {
+ throw new InvalidArgumentException(sprintf(
+ '%s(): Argument #1 ($client) must be of type %s or %s, `%s` given',
+ __METHOD__,
+ Client::class,
+ HttpClient::class,
+ (is_object($client)) ? get_class($client) : gettype($client)
+ ));
+ }
+
+ if ($client instanceof Client) {
+ $this->client = $client;
+ }
+
+ $httpClient = $client;
+
+ if (! $httpClient instanceof HttpClient) {
+ $httpClient = $this->handleClient($client);
+ }
+
+ $this->httpClient = $httpClient;
+ }
+
+ final protected function getHttpClient(): HttpClient
+ {
+ return $this->httpClient;
+ }
+
+ final protected function getLastResponse(): Response
+ {
+ return $this->lastResponse !== null ? $this->lastResponse : $this->createResponse(0, '', '');
}
/**
@@ -40,7 +87,13 @@ public function lastCallFailed()
{
@trigger_error('`' . __METHOD__ . '()` is deprecated since v2.1.0, use \Redmine\Client\Client::getLastResponseStatusCode() instead.', E_USER_DEPRECATED);
- $code = $this->client->getLastResponseStatusCode();
+ if ($this->lastResponse !== null) {
+ $code = $this->lastResponse->getStatusCode();
+ } elseif ($this->client !== null) {
+ $code = $this->client->getLastResponseStatusCode();
+ } else {
+ $code = 0;
+ }
return 200 !== $code && 201 !== $code;
}
@@ -55,10 +108,10 @@ public function lastCallFailed()
*/
protected function get($path, $decodeIfJson = true)
{
- $this->client->requestGet(strval($path));
+ $this->lastResponse = $this->getHttpClient()->request('GET', strval($path));
- $body = $this->client->getLastResponseBody();
- $contentType = $this->client->getLastResponseContentType();
+ $body = $this->lastResponse->getBody();
+ $contentType = $this->lastResponse->getContentType();
// if response is XML, return a SimpleXMLElement object
if ('' !== $body && 0 === strpos($contentType, 'application/xml')) {
@@ -82,16 +135,17 @@ protected function get($path, $decodeIfJson = true)
* @param string $path
* @param string $data
*
- * @return string|false
+ * @return string|SimpleXMLElement|false
*/
protected function post($path, $data)
{
- $this->client->requestPost($path, $data);
+ $this->lastResponse = $this->getHttpClient()->request('POST', strval($path), $data);
- $body = $this->client->getLastResponseBody();
+ $body = $this->lastResponse->getBody();
+ $contentType = $this->lastResponse->getContentType();
// if response is XML, return a SimpleXMLElement object
- if ('' !== $body && 0 === strpos($this->client->getLastResponseContentType(), 'application/xml')) {
+ if ('' !== $body && 0 === strpos($contentType, 'application/xml')) {
return new SimpleXMLElement($body);
}
@@ -104,16 +158,17 @@ protected function post($path, $data)
* @param string $path
* @param string $data
*
- * @return string|false
+ * @return string|SimpleXMLElement
*/
protected function put($path, $data)
{
- $this->client->requestPut($path, $data);
+ $this->lastResponse = $this->getHttpClient()->request('PUT', strval($path), $data);
- $body = $this->client->getLastResponseBody();
+ $body = $this->lastResponse->getBody();
+ $contentType = $this->lastResponse->getContentType();
// if response is XML, return a SimpleXMLElement object
- if ('' !== $body && 0 === strpos($this->client->getLastResponseContentType(), 'application/xml')) {
+ if ('' !== $body && 0 === strpos($contentType, 'application/xml')) {
return new SimpleXMLElement($body);
}
@@ -125,13 +180,13 @@ protected function put($path, $data)
*
* @param string $path
*
- * @return false|SimpleXMLElement|string
+ * @return string
*/
protected function delete($path)
{
- $this->client->requestDelete($path);
+ $this->lastResponse = $this->getHttpClient()->request('DELETE', strval($path));
- return $this->client->getLastResponseBody();
+ return $this->lastResponse->getBody();
}
/**
@@ -179,7 +234,7 @@ protected function retrieveAll($endpoint, array $params = [])
try {
$data = $this->retrieveData(strval($endpoint), $params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
@@ -203,9 +258,9 @@ protected function retrieveAll($endpoint, array $params = [])
protected function retrieveData(string $endpoint, array $params = []): array
{
if (empty($params)) {
- $this->client->requestGet($endpoint);
+ $this->lastResponse = $this->getHttpClient()->request('GET', strval($endpoint));
- return $this->getLastResponseBodyAsArray();
+ return $this->getResponseAsArray($this->lastResponse);
}
$params = $this->sanitizeParams(
@@ -232,11 +287,12 @@ protected function retrieveData(string $endpoint, array $params = []): array
$params['limit'] = $_limit;
$params['offset'] = $offset;
- $this->client->requestGet(
+ $this->lastResponse = $this->getHttpClient()->request(
+ 'GET',
PathSerializer::create($endpoint, $params)->getPath()
);
- $newDataSet = $this->getLastResponseBodyAsArray();
+ $newDataSet = $this->getResponseAsArray($this->lastResponse);
$returnData = array_merge_recursive($returnData, $newDataSet);
@@ -310,11 +366,10 @@ protected function attachCustomFieldXML(SimpleXMLElement $xml, array $fields)
*
* @throws SerializerException if response body could not be converted into array
*/
- private function getLastResponseBodyAsArray(): array
+ private function getResponseAsArray(Response $response): array
{
- $body = $this->client->getLastResponseBody();
-
- $contentType = $this->client->getLastResponseContentType();
+ $body = $response->getBody();
+ $contentType = $response->getContentType();
$returnData = null;
// parse XML
@@ -330,4 +385,70 @@ private function getLastResponseBodyAsArray(): array
return $returnData;
}
+
+ private function handleClient(Client $client): HttpClient
+ {
+ $responseFactory = Closure::fromCallable([$this, 'createResponse']);
+
+ return new class ($client, $responseFactory) implements HttpClient {
+ private $client;
+ private $responseFactory;
+
+ public function __construct(Client $client, Closure $responseFactory)
+ {
+ $this->client = $client;
+ $this->responseFactory = $responseFactory;
+ }
+
+ public function request(string $method, string $path, string $body = ''): Response
+ {
+ if ($method === 'POST') {
+ $this->client->requestPost($path, $body);
+ } elseif ($method === 'PUT') {
+ $this->client->requestPut($path, $body);
+ } elseif ($method === 'DELETE') {
+ $this->client->requestDelete($path);
+ } else {
+ $this->client->requestGet($path);
+ }
+
+ return ($this->responseFactory)(
+ $this->client->getLastResponseStatusCode(),
+ $this->client->getLastResponseContentType(),
+ $this->client->getLastResponseBody()
+ );
+ }
+ };
+ }
+
+ private function createResponse(int $statusCode, string $contentType, string $body): Response
+ {
+ return new class ($statusCode, $contentType, $body) implements Response {
+ private $statusCode;
+ private $contentType;
+ private $body;
+
+ public function __construct(int $statusCode, string $contentType, string $body)
+ {
+ $this->statusCode = $statusCode;
+ $this->contentType = $contentType;
+ $this->body = $body;
+ }
+
+ public function getStatusCode(): int
+ {
+ return $this->statusCode;
+ }
+
+ public function getContentType(): string
+ {
+ return $this->contentType;
+ }
+
+ public function getBody(): string
+ {
+ return $this->body;
+ }
+ };
+ }
}
diff --git a/src/Redmine/Api/CustomField.php b/src/Redmine/Api/CustomField.php
index 525fae43..1b9d10c6 100644
--- a/src/Redmine/Api/CustomField.php
+++ b/src/Redmine/Api/CustomField.php
@@ -55,7 +55,7 @@ public function all(array $params = [])
try {
$this->customFields = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Group.php b/src/Redmine/Api/Group.php
index 51101862..206dcc62 100644
--- a/src/Redmine/Api/Group.php
+++ b/src/Redmine/Api/Group.php
@@ -58,7 +58,7 @@ public function all(array $params = [])
try {
$this->groups = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Issue.php b/src/Redmine/Api/Issue.php
index 2bd3a0ae..3d3264cc 100644
--- a/src/Redmine/Api/Issue.php
+++ b/src/Redmine/Api/Issue.php
@@ -2,12 +2,15 @@
namespace Redmine\Api;
+use Redmine\Client\NativeCurlClient;
+use Redmine\Client\Psr18Client;
use Redmine\Exception;
use Redmine\Exception\SerializerException;
use Redmine\Exception\UnexpectedResponseException;
use Redmine\Serializer\JsonSerializer;
use Redmine\Serializer\PathSerializer;
use Redmine\Serializer\XmlSerializer;
+use SimpleXMLElement;
/**
* Listing issues, searching, editing and closing your projects issues.
@@ -24,6 +27,31 @@ class Issue extends AbstractApi
public const PRIO_URGENT = 4;
public const PRIO_IMMEDIATE = 5;
+ /**
+ * @var IssueCategory
+ */
+ private $issueCategoryApi;
+
+ /**
+ * @var IssueStatus
+ */
+ private $issueStatusApi;
+
+ /**
+ * @var Project
+ */
+ private $projectApi;
+
+ /**
+ * @var Tracker
+ */
+ private $trackerApi;
+
+ /**
+ * @var User
+ */
+ private $userApi;
+
/**
* List issues.
*
@@ -82,7 +110,7 @@ public function all(array $params = [])
try {
return $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
@@ -125,7 +153,7 @@ public function show($id, array $params = [])
*
* @param array $params the new issue data
*
- * @return string|false
+ * @return string|SimpleXMLElement|false
*/
public function create(array $params = [])
{
@@ -160,7 +188,7 @@ public function create(array $params = [])
*
* @param string $id the issue number
*
- * @return string|false
+ * @return string|SimpleXMLElement|false
*/
public function update($id, array $params)
{
@@ -219,15 +247,14 @@ public function removeWatcher($id, $watcherUserId)
* @param int $id
* @param string $status
*
- * @return string|false
+ * @return string|SimpleXMLElement|false
*/
public function setIssueStatus($id, $status)
{
- /** @var IssueStatus */
- $api = $this->client->getApi('issue_status');
+ $issueStatusApi = $this->getIssueStatusApi();
return $this->update($id, [
- 'status_id' => $api->getIdByName($status),
+ 'status_id' => $issueStatusApi->getIdByName($status),
]);
}
@@ -254,39 +281,44 @@ public function addNoteToIssue($id, $note, $privateNote = false)
private function cleanParams(array $params = [])
{
if (isset($params['project'])) {
- /** @var Project */
- $apiProject = $this->client->getApi('project');
- $params['project_id'] = $apiProject->getIdByName($params['project']);
+ $projectApi = $this->getProjectApi();
+
+ $params['project_id'] = $projectApi->getIdByName($params['project']);
unset($params['project']);
- if (isset($params['category'])) {
- /** @var IssueCategory */
- $apiIssueCategory = $this->client->getApi('issue_category');
- $params['category_id'] = $apiIssueCategory->getIdByName($params['project_id'], $params['category']);
- unset($params['category']);
- }
}
+
+ if (isset($params['category']) && isset($params['project_id'])) {
+ $issueCategoryApi = $this->getIssueCategoryApi();
+
+ $params['category_id'] = $issueCategoryApi->getIdByName($params['project_id'], $params['category']);
+ unset($params['category']);
+ }
+
if (isset($params['status'])) {
- /** @var IssueStatus */
- $apiIssueStatus = $this->client->getApi('issue_status');
- $params['status_id'] = $apiIssueStatus->getIdByName($params['status']);
+ $issueStatusApi = $this->getIssueStatusApi();
+
+ $params['status_id'] = $issueStatusApi->getIdByName($params['status']);
unset($params['status']);
}
+
if (isset($params['tracker'])) {
- /** @var Tracker */
- $apiTracker = $this->client->getApi('tracker');
- $params['tracker_id'] = $apiTracker->getIdByName($params['tracker']);
+ $trackerApi = $this->getTrackerApi();
+
+ $params['tracker_id'] = $trackerApi->getIdByName($params['tracker']);
unset($params['tracker']);
}
+
if (isset($params['assigned_to'])) {
- /** @var User */
- $apiUser = $this->client->getApi('user');
- $params['assigned_to_id'] = $apiUser->getIdByUsername($params['assigned_to']);
+ $userApi = $this->getUserApi();
+
+ $params['assigned_to_id'] = $userApi->getIdByUsername($params['assigned_to']);
unset($params['assigned_to']);
}
+
if (isset($params['author'])) {
- /** @var User */
- $apiUser = $this->client->getApi('user');
- $params['author_id'] = $apiUser->getIdByUsername($params['author']);
+ $userApi = $this->getUserApi();
+
+ $params['author_id'] = $userApi->getIdByUsername($params['author']);
unset($params['author']);
}
@@ -345,4 +377,99 @@ public function remove($id)
{
return $this->delete('/issues/' . $id . '.xml');
}
+
+ /**
+ * @return IssueCategory
+ */
+ private function getIssueCategoryApi()
+ {
+ if ($this->issueCategoryApi === null) {
+ if ($this->client !== null && ! $this->client instanceof NativeCurlClient && ! $this->client instanceof Psr18Client) {
+ /** @var IssueCategory */
+ $issueCategoryApi = $this->client->getApi('issue_category');
+ } else {
+ $issueCategoryApi = new IssueCategory($this->getHttpClient());
+ }
+
+ $this->issueCategoryApi = $issueCategoryApi;
+ }
+
+ return $this->issueCategoryApi;
+ }
+
+ /**
+ * @return IssueStatus
+ */
+ private function getIssueStatusApi()
+ {
+ if ($this->issueStatusApi === null) {
+ if ($this->client !== null && ! $this->client instanceof NativeCurlClient && ! $this->client instanceof Psr18Client) {
+ /** @var IssueStatus */
+ $issueStatusApi = $this->client->getApi('issue_status');
+ } else {
+ $issueStatusApi = new IssueStatus($this->getHttpClient());
+ }
+
+ $this->issueStatusApi = $issueStatusApi;
+ }
+
+ return $this->issueStatusApi;
+ }
+
+ /**
+ * @return Project
+ */
+ private function getProjectApi()
+ {
+ if ($this->projectApi === null) {
+ if ($this->client !== null && ! $this->client instanceof NativeCurlClient && ! $this->client instanceof Psr18Client) {
+ /** @var Project */
+ $projectApi = $this->client->getApi('project');
+ } else {
+ $projectApi = new Project($this->getHttpClient());
+ }
+
+ $this->projectApi = $projectApi;
+ }
+
+ return $this->projectApi;
+ }
+
+ /**
+ * @return Tracker
+ */
+ private function getTrackerApi()
+ {
+ if ($this->trackerApi === null) {
+ if ($this->client !== null && ! $this->client instanceof NativeCurlClient && ! $this->client instanceof Psr18Client) {
+ /** @var Tracker */
+ $trackerApi = $this->client->getApi('tracker');
+ } else {
+ $trackerApi = new Tracker($this->getHttpClient());
+ }
+
+ $this->trackerApi = $trackerApi;
+ }
+
+ return $this->trackerApi;
+ }
+
+ /**
+ * @return User
+ */
+ private function getUserApi()
+ {
+ if ($this->userApi === null) {
+ if ($this->client !== null && ! $this->client instanceof NativeCurlClient && ! $this->client instanceof Psr18Client) {
+ /** @var User */
+ $userApi = $this->client->getApi('user');
+ } else {
+ $userApi = new User($this->getHttpClient());
+ }
+
+ $this->userApi = $userApi;
+ }
+
+ return $this->userApi;
+ }
}
diff --git a/src/Redmine/Api/IssueCategory.php b/src/Redmine/Api/IssueCategory.php
index bac5d553..d42900f2 100644
--- a/src/Redmine/Api/IssueCategory.php
+++ b/src/Redmine/Api/IssueCategory.php
@@ -69,7 +69,7 @@ public function all($project, array $params = [])
try {
return $this->listByProject(strval($project), $params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/IssuePriority.php b/src/Redmine/Api/IssuePriority.php
index 917a9579..e3e777b3 100644
--- a/src/Redmine/Api/IssuePriority.php
+++ b/src/Redmine/Api/IssuePriority.php
@@ -55,7 +55,7 @@ public function all(array $params = [])
try {
$this->issuePriorities = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/IssueRelation.php b/src/Redmine/Api/IssueRelation.php
index 26b7f19b..113674e4 100644
--- a/src/Redmine/Api/IssueRelation.php
+++ b/src/Redmine/Api/IssueRelation.php
@@ -58,7 +58,7 @@ public function all($issueId, array $params = [])
try {
$this->relations = $this->listByIssueId($issueId, $params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/IssueStatus.php b/src/Redmine/Api/IssueStatus.php
index eec0e127..fa6fd577 100644
--- a/src/Redmine/Api/IssueStatus.php
+++ b/src/Redmine/Api/IssueStatus.php
@@ -55,7 +55,7 @@ public function all(array $params = [])
try {
$this->issueStatuses = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Membership.php b/src/Redmine/Api/Membership.php
index 62d8f90f..7650ea22 100644
--- a/src/Redmine/Api/Membership.php
+++ b/src/Redmine/Api/Membership.php
@@ -68,7 +68,7 @@ public function all($project, array $params = [])
try {
$this->memberships = $this->listByProject(strval($project), $params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/News.php b/src/Redmine/Api/News.php
index 6999ca3e..e6366e17 100644
--- a/src/Redmine/Api/News.php
+++ b/src/Redmine/Api/News.php
@@ -88,7 +88,7 @@ public function all($project = null, array $params = [])
$this->news = $this->listByProject(strval($project), $params);
}
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Project.php b/src/Redmine/Api/Project.php
index 259f1ae8..123086e5 100755
--- a/src/Redmine/Api/Project.php
+++ b/src/Redmine/Api/Project.php
@@ -58,7 +58,7 @@ public function all(array $params = [])
try {
$this->projects = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Query.php b/src/Redmine/Api/Query.php
index da629b87..475a551f 100644
--- a/src/Redmine/Api/Query.php
+++ b/src/Redmine/Api/Query.php
@@ -55,7 +55,7 @@ public function all(array $params = [])
try {
$this->query = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Role.php b/src/Redmine/Api/Role.php
index b01b5c6d..67832c89 100644
--- a/src/Redmine/Api/Role.php
+++ b/src/Redmine/Api/Role.php
@@ -55,7 +55,7 @@ public function all(array $params = [])
try {
$this->roles = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Search.php b/src/Redmine/Api/Search.php
index d9fc1fbb..f286b9c6 100644
--- a/src/Redmine/Api/Search.php
+++ b/src/Redmine/Api/Search.php
@@ -55,7 +55,7 @@ public function search($query, array $params = [])
try {
$this->results = $this->listByQuery($query, $params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/TimeEntry.php b/src/Redmine/Api/TimeEntry.php
index 74c49084..092f68e3 100644
--- a/src/Redmine/Api/TimeEntry.php
+++ b/src/Redmine/Api/TimeEntry.php
@@ -57,7 +57,7 @@ public function all(array $params = [])
try {
$this->timeEntries = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/TimeEntryActivity.php b/src/Redmine/Api/TimeEntryActivity.php
index 50c4fa13..871fe789 100644
--- a/src/Redmine/Api/TimeEntryActivity.php
+++ b/src/Redmine/Api/TimeEntryActivity.php
@@ -51,7 +51,7 @@ public function all(array $params = [])
try {
$this->timeEntryActivities = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Tracker.php b/src/Redmine/Api/Tracker.php
index 500d7ded..85f3507c 100644
--- a/src/Redmine/Api/Tracker.php
+++ b/src/Redmine/Api/Tracker.php
@@ -55,7 +55,7 @@ public function all(array $params = [])
try {
$this->trackers = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/User.php b/src/Redmine/Api/User.php
index 536c8b67..ca719a70 100644
--- a/src/Redmine/Api/User.php
+++ b/src/Redmine/Api/User.php
@@ -58,7 +58,7 @@ public function all(array $params = [])
try {
$this->users = $this->list($params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Version.php b/src/Redmine/Api/Version.php
index a77f6f95..3252c985 100644
--- a/src/Redmine/Api/Version.php
+++ b/src/Redmine/Api/Version.php
@@ -67,7 +67,7 @@ public function all($project, array $params = [])
try {
$this->versions = $this->listByProject(strval($project), $params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Api/Wiki.php b/src/Redmine/Api/Wiki.php
index d5c4ed35..7a066978 100644
--- a/src/Redmine/Api/Wiki.php
+++ b/src/Redmine/Api/Wiki.php
@@ -67,7 +67,7 @@ public function all($project, array $params = [])
try {
$this->wikiPages = $this->listByProject(strval($project), $params);
} catch (Exception $e) {
- if ($this->client->getLastResponseBody() === '') {
+ if ($this->getLastResponse()->getBody() === '') {
return false;
}
diff --git a/src/Redmine/Exception/ClientException.php b/src/Redmine/Exception/ClientException.php
index 426b227d..c81bc3f1 100644
--- a/src/Redmine/Exception/ClientException.php
+++ b/src/Redmine/Exception/ClientException.php
@@ -5,4 +5,9 @@
use Exception;
use Redmine\Exception as RedmineException;
+/**
+ * Client exception.
+ *
+ * Will be thrown if anything goes wrong on creating or sending a HTTP request
+ */
class ClientException extends Exception implements RedmineException {}
diff --git a/src/Redmine/Http/HttpClient.php b/src/Redmine/Http/HttpClient.php
new file mode 100644
index 00000000..0cc74fc4
--- /dev/null
+++ b/src/Redmine/Http/HttpClient.php
@@ -0,0 +1,28 @@
+createMock(Response::class);
+ $response->expects($this->any())->method('getStatusCode')->willReturn(200);
+ $response->expects($this->any())->method('getContentType')->willReturn('application/xml');
+ $response->expects($this->any())->method('getBody')->willReturn('');
+
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(1))->method('request')->with('DELETE', 'path.xml', '')->willReturn($response);
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'delete');
+ $method->setAccessible(true);
+
+ // Perform the tests
+ $return = $method->invoke($api, 'path.xml');
+
+ $this->assertSame('', $return);
+ }
+
+ /**
+ * @dataProvider getXmlDecodingFromDeleteMethodData
+ */
+ public function testXmlDecodingFromDeleteMethod($response, $expected)
+ {
+ $client = $this->createMock(Client::class);
+ $client->method('getLastResponseBody')->willReturn($response);
+ $client->method('getLastResponseContentType')->willReturn('application/xml');
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'delete');
+ $method->setAccessible(true);
+
+ // Perform the tests
+ $return = $method->invoke($api, 'path.xml');
+
+ $this->assertSame($expected, $return);
+ }
+
+ public static function getXmlDecodingFromDeleteMethodData(): array
+ {
+ return [
+ 'no decode by default' => ['', ''],
+ ];
+ }
+}
diff --git a/tests/Unit/Api/AbstractApi/GetTest.php b/tests/Unit/Api/AbstractApi/GetTest.php
new file mode 100644
index 00000000..29750b59
--- /dev/null
+++ b/tests/Unit/Api/AbstractApi/GetTest.php
@@ -0,0 +1,105 @@
+createMock(Response::class);
+ $response->expects($this->any())->method('getStatusCode')->willReturn(200);
+ $response->expects($this->any())->method('getContentType')->willReturn('application/json');
+ $response->expects($this->any())->method('getBody')->willReturn('{"foo_bar": 12345}');
+
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(1))->method('request')->with('GET', 'path.json')->willReturn($response);
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'get');
+ $method->setAccessible(true);
+
+ // Perform the tests
+ $this->assertSame(
+ ['foo_bar' => 12345],
+ $method->invoke($api, 'path.json')
+ );
+ }
+
+ /**
+ * @dataProvider getJsonDecodingFromGetMethodData
+ */
+ public function testJsonDecodingFromGetMethod($response, $decode, $expected)
+ {
+ $client = $this->createMock(Client::class);
+ $client->method('getLastResponseBody')->willReturn($response);
+ $client->method('getLastResponseContentType')->willReturn('application/json');
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'get');
+ $method->setAccessible(true);
+
+ // Perform the tests
+ if (is_bool($decode)) {
+ $this->assertSame($expected, $method->invoke($api, 'path', $decode));
+ } else {
+ $this->assertSame($expected, $method->invoke($api, 'path'));
+ }
+ }
+
+ public static function getJsonDecodingFromGetMethodData(): array
+ {
+ return [
+ 'test decode by default' => ['{"foo_bar": 12345}', null, ['foo_bar' => 12345]],
+ 'test decode by default, JSON decode: false' => ['{"foo_bar": 12345}', false, '{"foo_bar": 12345}'],
+ 'test decode by default, JSON decode: true' => ['{"foo_bar": 12345}', true, ['foo_bar' => 12345]],
+ 'Empty body, JSON decode: false' => ['', false, false],
+ 'Empty body, JSON decode: true' => ['', true, false],
+ 'test invalid JSON' => ['{"foo_bar":', true, 'Error decoding body as JSON: Syntax error'],
+ ];
+ }
+
+ /**
+ * @dataProvider getXmlDecodingFromGetMethodData
+ */
+ public function testXmlDecodingFromGetMethod($response, $decode, $expected)
+ {
+ $client = $this->createMock(Client::class);
+ $client->method('getLastResponseBody')->willReturn($response);
+ $client->method('getLastResponseContentType')->willReturn('application/xml');
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'get');
+ $method->setAccessible(true);
+
+ // Perform the tests
+ $return = $method->invoke($api, 'path', $decode);
+
+ $this->assertInstanceOf(SimpleXMLElement::class, $return);
+ $this->assertXmlStringEqualsXmlString($expected, $return->asXML());
+ }
+
+ public static function getXmlDecodingFromGetMethodData(): array
+ {
+ return [
+ 'decode by default' => ['', null, ''], // test decode by default
+ 'decode true' => ['', true, ''],
+ 'decode false' => ['', false, ''], // test that xml decoding will be always happen
+ ];
+ }
+}
diff --git a/tests/Unit/Api/AbstractApi/PostTest.php b/tests/Unit/Api/AbstractApi/PostTest.php
new file mode 100644
index 00000000..7f08ea62
--- /dev/null
+++ b/tests/Unit/Api/AbstractApi/PostTest.php
@@ -0,0 +1,69 @@
+createMock(Response::class);
+ $response->expects($this->any())->method('getStatusCode')->willReturn(200);
+ $response->expects($this->any())->method('getContentType')->willReturn('application/xml');
+ $response->expects($this->any())->method('getBody')->willReturn('');
+
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(1))->method('request')->with('POST', 'path.xml', '')->willReturn($response);
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'post');
+ $method->setAccessible(true);
+
+ // Perform the tests
+ $return = $method->invoke($api, 'path.xml', '');
+
+ $this->assertInstanceOf(SimpleXMLElement::class, $return);
+ $this->assertXmlStringEqualsXmlString('', $return->asXML());
+ }
+
+ /**
+ * @dataProvider getXmlDecodingFromPostMethodData
+ */
+ public function testXmlDecodingFromPostMethod($response, $expected)
+ {
+ $client = $this->createMock(Client::class);
+ $client->method('getLastResponseBody')->willReturn($response);
+ $client->method('getLastResponseContentType')->willReturn('application/xml');
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'post');
+ $method->setAccessible(true);
+
+ // Perform the tests
+ $return = $method->invoke($api, 'path.xml', '');
+
+ $this->assertInstanceOf(SimpleXMLElement::class, $return);
+ $this->assertXmlStringEqualsXmlString($expected, $return->asXML());
+ }
+
+ public static function getXmlDecodingFromPostMethodData(): array
+ {
+ return [
+ 'decode by default' => ['', ''], // test decode by default
+ ];
+ }
+}
diff --git a/tests/Unit/Api/AbstractApi/PutTest.php b/tests/Unit/Api/AbstractApi/PutTest.php
new file mode 100644
index 00000000..41071ecb
--- /dev/null
+++ b/tests/Unit/Api/AbstractApi/PutTest.php
@@ -0,0 +1,69 @@
+createMock(Response::class);
+ $response->expects($this->any())->method('getStatusCode')->willReturn(200);
+ $response->expects($this->any())->method('getContentType')->willReturn('application/xml');
+ $response->expects($this->any())->method('getBody')->willReturn('');
+
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(1))->method('request')->with('PUT', 'path.xml', '')->willReturn($response);
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'put');
+ $method->setAccessible(true);
+
+ // Perform the tests
+ $return = $method->invoke($api, 'path.xml', '');
+
+ $this->assertInstanceOf(SimpleXMLElement::class, $return);
+ $this->assertXmlStringEqualsXmlString('', $return->asXML());
+ }
+
+ /**
+ * @dataProvider getXmlDecodingFromPutMethodData
+ */
+ public function testXmlDecodingFromPutMethod($response, $expected)
+ {
+ $client = $this->createMock(Client::class);
+ $client->method('getLastResponseBody')->willReturn($response);
+ $client->method('getLastResponseContentType')->willReturn('application/xml');
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'put');
+ $method->setAccessible(true);
+
+ // Perform the tests
+ $return = $method->invoke($api, 'path.xml', '');
+
+ $this->assertInstanceOf(SimpleXMLElement::class, $return);
+ $this->assertXmlStringEqualsXmlString($expected, $return->asXML());
+ }
+
+ public static function getXmlDecodingFromPutMethodData(): array
+ {
+ return [
+ 'decode by default' => ['', ''], // test decode by default
+ ];
+ }
+}
diff --git a/tests/Unit/Api/AbstractApiTest.php b/tests/Unit/Api/AbstractApiTest.php
index 42e6eb21..4cb0c355 100644
--- a/tests/Unit/Api/AbstractApiTest.php
+++ b/tests/Unit/Api/AbstractApiTest.php
@@ -2,10 +2,13 @@
namespace Redmine\Tests\Unit\Api;
+use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Redmine\Api\AbstractApi;
use Redmine\Client\Client;
use Redmine\Exception\SerializerException;
+use Redmine\Http\HttpClient;
+use Redmine\Http\Response;
use ReflectionMethod;
use SimpleXMLElement;
@@ -14,6 +17,53 @@
*/
class AbstractApiTest extends TestCase
{
+ public function testCreateWithHttpClientWorks()
+ {
+ $client = $this->createMock(HttpClient::class);
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'getHttpClient');
+ $method->setAccessible(true);
+
+ $this->assertSame($client, $method->invoke($api));
+ }
+
+ public function testCreateWitClientWorks()
+ {
+ $client = $this->createMock(Client::class);
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'getHttpClient');
+ $method->setAccessible(true);
+
+ $this->assertInstanceOf(HttpClient::class, $method->invoke($api));
+ }
+
+ public function testCreateWithoutClitentOrHttpClientThrowsException()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Redmine\Api\AbstractApi::__construct(): Argument #1 ($client) must be of type Redmine\Client\Client or Redmine\Http\HttpClient, `stdClass` given');
+
+ new class (new \stdClass()) extends AbstractApi {};
+ }
+
+ /**
+ * @covers ::getLastResponse
+ */
+ public function testGetLastResponseWithHttpClientWorks()
+ {
+ $client = $this->createMock(HttpClient::class);
+
+ $api = new class ($client) extends AbstractApi {};
+
+ $method = new ReflectionMethod($api, 'getLastResponse');
+ $method->setAccessible(true);
+
+ $this->assertInstanceOf(Response::class, $method->invoke($api));
+ }
+
/**
* @test
* @dataProvider getIsNotNullReturnsCorrectBooleanData
@@ -53,11 +103,51 @@ public static function getIsNotNullReturnsCorrectBooleanData(): array
}
/**
- * @covers \Redmine\Api\AbstractApi
+ * @covers ::lastCallFailed
+ */
+ public function testLastCallFailedPreventsRaceCondition()
+ {
+ $response1 = $this->createMock(Response::class);
+ $response1->method('getStatusCode')->willReturn(200);
+
+ $response2 = $this->createMock(Response::class);
+ $response2->method('getStatusCode')->willReturn(500);
+
+ $client = $this->createMock(HttpClient::class);
+ $client->method('request')->willReturnMap([
+ ['GET', '200.json', '', $response1],
+ ['GET', '500.json', '', $response2],
+ ]);
+
+ $api1 = new class ($client) extends AbstractApi {
+ public function __construct($client)
+ {
+ parent::__construct($client);
+ parent::get('200.json', false);
+ }
+ };
+
+ $api2 = new class ($client) extends AbstractApi {
+ public function __construct($client)
+ {
+ parent::__construct($client);
+ parent::get('500.json', false);
+ }
+ };
+
+ $api3 = new class ($client) extends AbstractApi {};
+
+ $this->assertSame(false, $api1->lastCallFailed());
+ $this->assertSame(true, $api2->lastCallFailed());
+ $this->assertSame(true, $api3->lastCallFailed());
+ }
+
+ /**
+ * @covers ::lastCallFailed
* @test
* @dataProvider getLastCallFailedData
*/
- public function testLastCallFailedReturnsCorrectBoolean($statusCode, $expectedBoolean)
+ public function testLastCallFailedWithClientReturnsCorrectBoolean($statusCode, $expectedBoolean)
{
$client = $this->createMock(Client::class);
$client->method('getLastResponseStatusCode')->willReturn($statusCode);
@@ -67,9 +157,34 @@ public function testLastCallFailedReturnsCorrectBoolean($statusCode, $expectedBo
$this->assertSame($expectedBoolean, $api->lastCallFailed());
}
+ /**
+ * @covers ::lastCallFailed
+ * @test
+ * @dataProvider getLastCallFailedData
+ */
+ public function testLastCallFailedWithHttpClientReturnsCorrectBoolean($statusCode, $expectedBoolean)
+ {
+ $response = $this->createMock(Response::class);
+ $response->method('getStatusCode')->willReturn($statusCode);
+
+ $client = $this->createMock(HttpClient::class);
+ $client->method('request')->willReturn($response);
+
+ $api = new class ($client) extends AbstractApi {
+ public function __construct($client)
+ {
+ parent::__construct($client);
+ $this->get('', false);
+ }
+ };
+
+ $this->assertSame($expectedBoolean, $api->lastCallFailed());
+ }
+
public static function getLastCallFailedData(): array
{
return [
+ [0, true],
[100, true],
[101, true],
[102, true],
@@ -138,89 +253,7 @@ public static function getLastCallFailedData(): array
}
/**
- * @covers \Redmine\Api\AbstractApi
- * @test
- * @dataProvider getJsonDecodingFromGetMethodData
- */
- public function testJsonDecodingFromGetMethod($response, $decode, $expected)
- {
- $client = $this->createMock(Client::class);
- $client->method('getLastResponseBody')->willReturn($response);
- $client->method('getLastResponseContentType')->willReturn('application/json');
-
- $api = new class ($client) extends AbstractApi {};
-
- $method = new ReflectionMethod($api, 'get');
- $method->setAccessible(true);
-
- // Perform the tests
- if (is_bool($decode)) {
- $this->assertSame($expected, $method->invoke($api, 'path', $decode));
- } else {
- $this->assertSame($expected, $method->invoke($api, 'path'));
- }
- }
-
- public static function getJsonDecodingFromGetMethodData(): array
- {
- return [
- 'test decode by default' => ['{"foo_bar": 12345}', null, ['foo_bar' => 12345]],
- 'test decode by default, JSON decode: false' => ['{"foo_bar": 12345}', false, '{"foo_bar": 12345}'],
- 'test decode by default, JSON decode: true' => ['{"foo_bar": 12345}', true, ['foo_bar' => 12345]],
- 'Empty body, JSON decode: false' => ['', false, false],
- 'Empty body, JSON decode: true' => ['', true, false],
- 'test invalid JSON' => ['{"foo_bar":', true, 'Error decoding body as JSON: Syntax error'],
- ];
- }
-
- /**
- * @covers \Redmine\Api\AbstractApi
- * @test
- * @dataProvider getXmlDecodingFromGetMethodData
- */
- public function testXmlDecodingFromRequestMethods($methodName, $response, $decode, $expected)
- {
- $client = $this->createMock(Client::class);
- $client->method('getLastResponseBody')->willReturn($response);
- $client->method('getLastResponseContentType')->willReturn('application/xml');
-
- $api = new class ($client) extends AbstractApi {};
-
- $method = new ReflectionMethod($api, $methodName);
- $method->setAccessible(true);
-
- // Perform the tests
- if ('get' === $methodName) {
- $return = $method->invoke($api, 'path', $decode);
-
- $this->assertInstanceOf(SimpleXMLElement::class, $return);
- $this->assertXmlStringEqualsXmlString($expected, $return->asXML());
- } elseif ('delete' === $methodName) {
- $return = $method->invoke($api, 'path');
-
- $this->assertSame($expected, $return);
- } else {
- $return = $method->invoke($api, 'path', '');
-
- $this->assertInstanceOf(SimpleXMLElement::class, $return);
- $this->assertXmlStringEqualsXmlString($expected, $return->asXML());
- }
- }
-
- public static function getXmlDecodingFromGetMethodData(): array
- {
- return [
- ['get', '', null, ''], // test decode by default
- ['get', '', true, ''],
- ['get', '', false, ''], // test that xml decoding will be always happen
- ['post', '', null, ''],
- ['put', '', null, ''],
- ['delete', '', null, ''],
- ];
- }
-
- /**
- * @covers \Redmine\Api\AbstractApi::retrieveData
+ * @covers ::retrieveData
*
* @dataProvider retrieveDataData
*/
@@ -247,7 +280,7 @@ public static function retrieveDataData(): array
}
/**
- * @covers \Redmine\Api\AbstractApi::retrieveData
+ * @covers ::retrieveData
*
* @dataProvider getRetrieveDataToExceptionData
*/
@@ -277,7 +310,7 @@ public static function getRetrieveDataToExceptionData(): array
}
/**
- * @covers \Redmine\Api\AbstractApi::retrieveAll
+ * @covers ::retrieveAll
*
* @dataProvider getRetrieveAllData
*/
@@ -306,7 +339,7 @@ public static function getRetrieveAllData(): array
}
/**
- * @covers \Redmine\Api\AbstractApi::attachCustomFieldXML
+ * @covers ::attachCustomFieldXML
*/
public function testDeprecatedAttachCustomFieldXML()
{
diff --git a/tests/Unit/Api/AttachmentTest.php b/tests/Unit/Api/AttachmentTest.php
index 5bd3df46..dff94bbe 100644
--- a/tests/Unit/Api/AttachmentTest.php
+++ b/tests/Unit/Api/AttachmentTest.php
@@ -61,7 +61,6 @@ public static function responseCodeProvider(): array
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -90,7 +89,6 @@ public function testShowReturnsClientGetResponse()
/**
* Test download().
*
- * @covers ::get
* @covers ::download
* @test
*/
diff --git a/tests/Unit/Api/CustomFieldTest.php b/tests/Unit/Api/CustomFieldTest.php
index dba2bd2d..030a4915 100644
--- a/tests/Unit/Api/CustomFieldTest.php
+++ b/tests/Unit/Api/CustomFieldTest.php
@@ -44,9 +44,6 @@ function ($errno, $errstr): bool {
* Test all().
*
* @covers ::all
- * @covers ::get
- * @covers ::retrieveAll
- * @covers ::isNotNull
* @dataProvider getAllData
* @test
*/
@@ -85,9 +82,6 @@ public static function getAllData(): array
* Test all().
*
* @covers ::all
- * @covers ::get
- * @covers ::retrieveAll
- * @covers ::isNotNull
* @test
*/
public function testAllReturnsClientGetResponseWithParameters()
@@ -123,9 +117,6 @@ public function testAllReturnsClientGetResponseWithParameters()
* Test all().
*
* @covers ::all
- * @covers ::get
- * @covers ::retrieveAll
- * @covers ::isNotNull
* @test
*/
public function testAllReturnsClientGetResponseWithHighLimit()
@@ -164,9 +155,6 @@ public function testAllReturnsClientGetResponseWithHighLimit()
* Test all().
*
* @covers ::all
- * @covers ::get
- * @covers ::retrieveAll
- * @covers ::isNotNull
* @test
*/
public function testAllCallsEndpointUntilOffsetIsHigherThanTotalCount()
diff --git a/tests/Unit/Api/GroupTest.php b/tests/Unit/Api/GroupTest.php
index d6081c6d..fa5fc56a 100644
--- a/tests/Unit/Api/GroupTest.php
+++ b/tests/Unit/Api/GroupTest.php
@@ -233,7 +233,6 @@ public function testListingCallsGetEveryTimeWithForceUpdate()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -266,7 +265,6 @@ public function testShowReturnsClientGetResponse()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -332,7 +330,7 @@ public function testRemoveCallsDelete()
$client->expects($this->exactly(1))
->method('getLastResponseBody')
->willReturn($response);
- $client->expects($this->exactly(0))
+ $client->expects($this->exactly(1))
->method('getLastResponseContentType')
->willReturn('application/json');
diff --git a/tests/Unit/Api/IssueCategoryTest.php b/tests/Unit/Api/IssueCategoryTest.php
index d9cba882..43dc5ae2 100644
--- a/tests/Unit/Api/IssueCategoryTest.php
+++ b/tests/Unit/Api/IssueCategoryTest.php
@@ -237,7 +237,6 @@ public function testListingCallsGetEveryTimeWithForceUpdate()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
diff --git a/tests/Unit/Api/IssueRelationTest.php b/tests/Unit/Api/IssueRelationTest.php
index a75e3708..439722fb 100644
--- a/tests/Unit/Api/IssueRelationTest.php
+++ b/tests/Unit/Api/IssueRelationTest.php
@@ -119,7 +119,6 @@ public function testAllReturnsClientGetResponseWithParametersAndProject()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -154,7 +153,6 @@ public function testShowReturnsClientGetResponse()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -213,7 +211,7 @@ public function testRemoveCallsDelete()
$client->expects($this->exactly(1))
->method('getLastResponseBody')
->willReturn($response);
- $client->expects($this->exactly(0))
+ $client->expects($this->exactly(1))
->method('getLastResponseContentType')
->willReturn('application/json');
diff --git a/tests/Unit/Api/IssueTest.php b/tests/Unit/Api/IssueTest.php
index 50390650..582e8c38 100644
--- a/tests/Unit/Api/IssueTest.php
+++ b/tests/Unit/Api/IssueTest.php
@@ -5,7 +5,10 @@
use PHPUnit\Framework\TestCase;
use Redmine\Api\Issue;
use Redmine\Client\Client;
+use Redmine\Http\HttpClient;
+use Redmine\Http\Response;
use Redmine\Tests\Fixtures\MockClient;
+use SimpleXMLElement;
/**
* @coversDefaultClass \Redmine\Api\Issue
@@ -142,7 +145,6 @@ public function testAllReturnsClientGetResponseWithParameters()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -175,7 +177,6 @@ public function testShowReturnsClientGetResponse()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -433,9 +434,14 @@ public function testCreateCallsPost()
*
* @covers ::create
* @covers ::cleanParams
+ * @covers ::getIssueCategoryApi
+ * @covers ::getIssueStatusApi
+ * @covers ::getProjectApi
+ * @covers ::getTrackerApi
+ * @covers ::getUserApi
* @test
*/
- public function testCreateCleansParameters()
+ public function testCreateWithClientCleansParameters()
{
// Test values
$response = 'API Response';
@@ -463,7 +469,7 @@ public function testCreateCleansParameters()
->willReturn('cleanedValue');
$client = $this->createMock(Client::class);
- $client->expects($this->exactly(6))
+ $client->expects($this->exactly(5))
->method('getApi')
->willReturnMap(
[
@@ -502,6 +508,276 @@ public function testCreateCleansParameters()
$this->assertSame($response, $api->create($parameters));
}
+ /**
+ * @covers ::create
+ * @covers ::cleanParams
+ * @covers ::getIssueStatusApi
+ * @test
+ */
+ public function testCreateWithHttpClientRetrievesIssueStatusId()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(2))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ if ($method === 'GET') {
+ $this->assertSame('/issue_statuses.json', $path);
+ $this->assertSame('', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/json',
+ 'getBody' => '{"issue_statuses":[{"name":"Status Name","id":123}]}',
+ ]
+ );
+ }
+
+ if ($method === 'POST') {
+ $this->assertSame('/issues.xml', $path);
+ $this->assertXmlStringEqualsXmlString('123', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ }
+
+ throw new \Exception();
+ });
+
+ // Create the object under test
+ $api = new Issue($client);
+
+ $xmlElement = $api->create(['status' => 'Status Name']);
+
+ // Perform the tests
+ $this->assertInstanceOf(SimpleXMLElement::class, $xmlElement);
+ $this->assertXmlStringEqualsXmlString(
+ '',
+ $xmlElement->asXml(),
+ );
+ }
+
+ /**
+ * @covers ::create
+ * @covers ::cleanParams
+ * @covers ::getProjectApi
+ * @test
+ */
+ public function testCreateWithHttpClientRetrievesProjectId()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(2))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ if ($method === 'GET') {
+ $this->assertSame('/projects.json', $path);
+ $this->assertSame('', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/json',
+ 'getBody' => '{"projects":[{"name":"Project Name","id":3}]}',
+ ]
+ );
+ }
+
+ if ($method === 'POST') {
+ $this->assertSame('/issues.xml', $path);
+ $this->assertXmlStringEqualsXmlString('3', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ }
+
+ throw new \Exception();
+ });
+
+ // Create the object under test
+ $api = new Issue($client);
+
+ $xmlElement = $api->create(['project' => 'Project Name']);
+
+ // Perform the tests
+ $this->assertInstanceOf(SimpleXMLElement::class, $xmlElement);
+ $this->assertXmlStringEqualsXmlString(
+ '',
+ $xmlElement->asXml(),
+ );
+ }
+
+ /**
+ * @covers ::create
+ * @covers ::cleanParams
+ * @covers ::getIssueCategoryApi
+ * @test
+ */
+ public function testCreateWithHttpClientRetrievesIssueCategoryId()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(2))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ if ($method === 'GET') {
+ $this->assertSame('/projects/3/issue_categories.json', $path);
+ $this->assertSame('', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/json',
+ 'getBody' => '{"issue_categories":[{"name":"Category Name","id":45}]}',
+ ]
+ );
+ }
+
+ if ($method === 'POST') {
+ $this->assertSame('/issues.xml', $path);
+ $this->assertXmlStringEqualsXmlString('345', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ }
+
+ throw new \Exception();
+ });
+
+ // Create the object under test
+ $api = new Issue($client);
+
+ $xmlElement = $api->create(['project_id' => 3, 'category' => 'Category Name']);
+
+ // Perform the tests
+ $this->assertInstanceOf(SimpleXMLElement::class, $xmlElement);
+ $this->assertXmlStringEqualsXmlString(
+ '',
+ $xmlElement->asXml(),
+ );
+ }
+
+ /**
+ * @covers ::create
+ * @covers ::cleanParams
+ * @covers ::getTrackerApi
+ * @test
+ */
+ public function testCreateWithHttpClientRetrievesTrackerId()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(2))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ if ($method === 'GET') {
+ $this->assertSame('/trackers.json', $path);
+ $this->assertSame('', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/json',
+ 'getBody' => '{"trackers":[{"name":"Tracker Name","id":9}]}',
+ ]
+ );
+ }
+
+ if ($method === 'POST') {
+ $this->assertSame('/issues.xml', $path);
+ $this->assertXmlStringEqualsXmlString('9', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ }
+
+ throw new \Exception();
+ });
+
+ // Create the object under test
+ $api = new Issue($client);
+
+ $xmlElement = $api->create(['tracker' => 'Tracker Name']);
+
+ // Perform the tests
+ $this->assertInstanceOf(SimpleXMLElement::class, $xmlElement);
+ $this->assertXmlStringEqualsXmlString(
+ '',
+ $xmlElement->asXml(),
+ );
+ }
+
+ /**
+ * @covers ::create
+ * @covers ::cleanParams
+ * @covers ::getUserApi
+ * @test
+ */
+ public function testCreateWithHttpClientRetrievesUserId()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(2))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ if ($method === 'GET') {
+ $this->assertSame('/users.json', $path);
+ $this->assertSame('', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/json',
+ 'getBody' => '{"users":[{"login":"Author Name","id":5},{"login":"Assigned to User Name","id":6}]}',
+ ]
+ );
+ }
+
+ if ($method === 'POST') {
+ $this->assertSame('/issues.xml', $path);
+ $this->assertXmlStringEqualsXmlString('65', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ }
+
+ throw new \Exception();
+ });
+
+ // Create the object under test
+ $api = new Issue($client);
+
+ $xmlElement = $api->create(['assigned_to' => 'Assigned to User Name', 'author' => 'Author Name']);
+
+ // Perform the tests
+ $this->assertInstanceOf(SimpleXMLElement::class, $xmlElement);
+ $this->assertXmlStringEqualsXmlString(
+ '',
+ $xmlElement->asXml(),
+ );
+ }
+
/**
* Test create() and buildXML().
*
@@ -624,7 +900,7 @@ public function testUpdateCleansParameters()
->willReturn('cleanedValue');
$client = $this->createMock(Client::class);
- $client->expects($this->exactly(6))
+ $client->expects($this->exactly(5))
->method('getApi')
->willReturnMap(
[
@@ -670,13 +946,13 @@ public function testUpdateCleansParameters()
* @covers ::setIssueStatus
* @test
*/
- public function testSetIssueStatus()
+ public function testSetIssueStatusWithClient()
{
// Test values
$response = 'API Response';
// Create the used mock objects
- $issueStatusApi = $this->createMock('Redmine\Api\Project');
+ $issueStatusApi = $this->createMock('Redmine\Api\IssueStatus');
$issueStatusApi->expects($this->once())
->method('getIdByName')
->willReturn(123);
@@ -710,6 +986,60 @@ public function testSetIssueStatus()
$this->assertSame($response, $api->setIssueStatus(5, 'Status Name'));
}
+ /**
+ * Test setIssueStatus().
+ *
+ * @covers ::setIssueStatus
+ * @test
+ */
+ public function testSetIssueStatusWithHttpClient()
+ {
+ $client = $this->createMock(HttpClient::class);
+ $client->expects($this->exactly(2))
+ ->method('request')
+ ->willReturnCallback(function (string $method, string $path, string $body = '') {
+ if ($method === 'GET') {
+ $this->assertSame('/issue_statuses.json', $path);
+ $this->assertSame('', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/json',
+ 'getBody' => '{"issue_statuses":[{"name":"Status Name","id":123}]}',
+ ]
+ );
+ }
+
+ if ($method === 'PUT') {
+ $this->assertSame('/issues/5.xml', $path);
+ $this->assertXmlStringEqualsXmlString('5123', $body);
+
+ return $this->createConfiguredMock(
+ Response::class,
+ [
+ 'getContentType' => 'application/xml',
+ 'getBody' => '',
+ ]
+ );
+ }
+
+ throw new \Exception();
+ });
+
+ // Create the object under test
+ $api = new Issue($client);
+
+ $xmlElement = $api->setIssueStatus(5, 'Status Name');
+
+ // Perform the tests
+ $this->assertInstanceOf(SimpleXMLElement::class, $xmlElement);
+ $this->assertXmlStringEqualsXmlString(
+ '',
+ $xmlElement->asXml(),
+ );
+ }
+
/**
* Test addNoteToIssue().
*
diff --git a/tests/Unit/Api/MembershipTest.php b/tests/Unit/Api/MembershipTest.php
index f9658864..f8522488 100644
--- a/tests/Unit/Api/MembershipTest.php
+++ b/tests/Unit/Api/MembershipTest.php
@@ -180,7 +180,7 @@ public function testRemoveMemberCallsDelete()
->method('requestDelete')
->with($this->stringContains('/memberships/5.xml'))
->willReturn(true);
- $client->expects($this->once())
+ $client->expects($this->exactly(2))
->method('getLastResponseContentType')
->willReturn('application/json');
$matcher = $this->exactly(2);
diff --git a/tests/Unit/Api/ProjectTest.php b/tests/Unit/Api/ProjectTest.php
index 1d177306..de28a6c6 100644
--- a/tests/Unit/Api/ProjectTest.php
+++ b/tests/Unit/Api/ProjectTest.php
@@ -122,7 +122,6 @@ public function testAllReturnsClientGetResponseWithParameters()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -158,7 +157,6 @@ public function testShowReturnsClientGetResponse()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
diff --git a/tests/Unit/Api/RoleTest.php b/tests/Unit/Api/RoleTest.php
index 38022ceb..381042fe 100644
--- a/tests/Unit/Api/RoleTest.php
+++ b/tests/Unit/Api/RoleTest.php
@@ -232,7 +232,6 @@ public function testListingCallsGetEveryTimeWithForceUpdate()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
diff --git a/tests/Unit/Api/TimeEntryTest.php b/tests/Unit/Api/TimeEntryTest.php
index abf7c33a..8b01c32a 100644
--- a/tests/Unit/Api/TimeEntryTest.php
+++ b/tests/Unit/Api/TimeEntryTest.php
@@ -126,7 +126,6 @@ public function testAllReturnsClientGetResponseWithParameters()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
diff --git a/tests/Unit/Api/UserTest.php b/tests/Unit/Api/UserTest.php
index 98b65d84..33d0d767 100644
--- a/tests/Unit/Api/UserTest.php
+++ b/tests/Unit/Api/UserTest.php
@@ -198,7 +198,6 @@ public function testAllReturnsClientGetResponseWithParameters()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -236,7 +235,6 @@ public function testShowReturnsClientGetResponse()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
diff --git a/tests/Unit/Api/VersionTest.php b/tests/Unit/Api/VersionTest.php
index e05bc9d2..4f4ffa1f 100644
--- a/tests/Unit/Api/VersionTest.php
+++ b/tests/Unit/Api/VersionTest.php
@@ -125,7 +125,6 @@ public function testAllReturnsClientGetResponseWithParameters()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -158,7 +157,6 @@ public function testShowWithNumericIdReturnsClientGetResponse()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
diff --git a/tests/Unit/Api/WikiTest.php b/tests/Unit/Api/WikiTest.php
index 65b4bd95..2ac36674 100644
--- a/tests/Unit/Api/WikiTest.php
+++ b/tests/Unit/Api/WikiTest.php
@@ -123,7 +123,6 @@ public function testAllReturnsClientGetResponseWithParameters()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -152,7 +151,6 @@ public function testShowWithNumericIdsReturnsClientGetResponse()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -181,7 +179,6 @@ public function testShowWithIdentifierReturnsClientGetResponse()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/
@@ -210,7 +207,6 @@ public function testShowWithNumericIdsAndVersionReturnsClientGetResponse()
/**
* Test show().
*
- * @covers ::get
* @covers ::show
* @test
*/