From 38f639a32d7a841eb6674c00bd4b1495e1e4f980 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 10 Mar 2026 21:03:19 +0100 Subject: [PATCH 1/5] feat: create Future class --- src/Redmine/Future.php | 16 ++++++++++++++++ tests/Unit/FutureTest.php | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 src/Redmine/Future.php create mode 100644 tests/Unit/FutureTest.php diff --git a/src/Redmine/Future.php b/src/Redmine/Future.php new file mode 100644 index 00000000..61e0a159 --- /dev/null +++ b/src/Redmine/Future.php @@ -0,0 +1,16 @@ + Date: Tue, 10 Mar 2026 21:08:46 +0100 Subject: [PATCH 2/5] feat: create Future::enableForwardCompatibility() method --- src/Redmine/Future.php | 9 ++++++++- tests/Unit/FutureTest.php | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Redmine/Future.php b/src/Redmine/Future.php index 61e0a159..3b72e1c8 100644 --- a/src/Redmine/Future.php +++ b/src/Redmine/Future.php @@ -9,8 +9,15 @@ */ final class Future { + private static bool $isForwardCompatibilityEnabled = false; + + public static function enableForwardCompatibility(): void + { + self::$isForwardCompatibilityEnabled = true; + } + public static function isForwardCompatibilityEnabled(): bool { - return false; + return self::$isForwardCompatibilityEnabled; } } diff --git a/tests/Unit/FutureTest.php b/tests/Unit/FutureTest.php index 002a2272..e17b7168 100644 --- a/tests/Unit/FutureTest.php +++ b/tests/Unit/FutureTest.php @@ -13,4 +13,11 @@ public function testIsForwardCompatabilityEnabledReturnsFalseByDefault(): void { self::assertFalse(Future::isForwardCompatibilityEnabled()); } + + public function testEnableForwardCompatabilityLetIsForwardCompatabilityEnabledReturnsTrue(): void + { + Future::enableForwardCompatibility(); + + self::assertTrue(Future::isForwardCompatibilityEnabled()); + } } From 2de02d8f87b09a4246f356f698d14121dea6fe68 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 10 Mar 2026 21:09:11 +0100 Subject: [PATCH 3/5] feat: create Future::disableForwardCompatibility() method --- src/Redmine/Future.php | 5 +++++ tests/Unit/FutureTest.php | 9 ++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Redmine/Future.php b/src/Redmine/Future.php index 3b72e1c8..a4e9b61c 100644 --- a/src/Redmine/Future.php +++ b/src/Redmine/Future.php @@ -16,6 +16,11 @@ public static function enableForwardCompatibility(): void self::$isForwardCompatibilityEnabled = true; } + public static function disableForwardCompatibility(): void + { + self::$isForwardCompatibilityEnabled = false; + } + public static function isForwardCompatibilityEnabled(): bool { return self::$isForwardCompatibilityEnabled; diff --git a/tests/Unit/FutureTest.php b/tests/Unit/FutureTest.php index e17b7168..10d0e7d5 100644 --- a/tests/Unit/FutureTest.php +++ b/tests/Unit/FutureTest.php @@ -14,10 +14,17 @@ public function testIsForwardCompatabilityEnabledReturnsFalseByDefault(): void self::assertFalse(Future::isForwardCompatibilityEnabled()); } - public function testEnableForwardCompatabilityLetIsForwardCompatabilityEnabledReturnsTrue(): void + public function testEnableForwardCompatabilityLetsIsForwardCompatabilityEnabledReturnTrue(): void { Future::enableForwardCompatibility(); self::assertTrue(Future::isForwardCompatibilityEnabled()); } + + public function testDisableForwardCompatabilityLetsIsForwardCompatabilityEnabledReturnFalse(): void + { + Future::disableForwardCompatibility(); + + self::assertFalse(Future::isForwardCompatibilityEnabled()); + } } From 67dd1e5745c2fb95fc8b8ced6dcae855247bac9d Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 10 Mar 2026 21:27:36 +0100 Subject: [PATCH 4/5] feat: implement enableFutureMode() in Client implementations --- src/Redmine/Client/ClientApiTrait.php | 6 ++++ .../NativeCurlClient/EnableFutureModeTest.php | 29 +++++++++++++++ .../Psr18Client/EnableFutureModeTest.php | 35 +++++++++++++++++++ tests/Unit/FutureTest.php | 2 ++ 4 files changed, 72 insertions(+) create mode 100644 tests/Unit/Client/NativeCurlClient/EnableFutureModeTest.php create mode 100644 tests/Unit/Client/Psr18Client/EnableFutureModeTest.php diff --git a/src/Redmine/Client/ClientApiTrait.php b/src/Redmine/Client/ClientApiTrait.php index c0e71859..f18885ad 100644 --- a/src/Redmine/Client/ClientApiTrait.php +++ b/src/Redmine/Client/ClientApiTrait.php @@ -4,6 +4,7 @@ use Redmine\Api; use Redmine\Exception\InvalidApiNameException; +use Redmine\Future; /** * Provide API instantiation to clients. @@ -60,6 +61,11 @@ public function getApi(string $name): Api return $this->apiInstances[$name]; } + public function enableFutureMode(): void + { + Future::enableForwardCompatibility(); + } + private function isUploadCall(string $path): bool { $path = strtolower($path); diff --git a/tests/Unit/Client/NativeCurlClient/EnableFutureModeTest.php b/tests/Unit/Client/NativeCurlClient/EnableFutureModeTest.php new file mode 100644 index 00000000..daff3a28 --- /dev/null +++ b/tests/Unit/Client/NativeCurlClient/EnableFutureModeTest.php @@ -0,0 +1,29 @@ +enableFutureMode(); + + self::assertTrue(Future::isForwardCompatibilityEnabled()); + + Future::disableForwardCompatibility(); + } +} diff --git a/tests/Unit/Client/Psr18Client/EnableFutureModeTest.php b/tests/Unit/Client/Psr18Client/EnableFutureModeTest.php new file mode 100644 index 00000000..a77f27c9 --- /dev/null +++ b/tests/Unit/Client/Psr18Client/EnableFutureModeTest.php @@ -0,0 +1,35 @@ +createStub(ClientInterface::class), + $this->createStub(RequestFactoryInterface::class), + $this->createStub(StreamFactoryInterface::class), + '', + '', + ); + $client->enableFutureMode(); + + self::assertTrue(Future::isForwardCompatibilityEnabled()); + + Future::disableForwardCompatibility(); + } +} diff --git a/tests/Unit/FutureTest.php b/tests/Unit/FutureTest.php index 10d0e7d5..1b15e8cf 100644 --- a/tests/Unit/FutureTest.php +++ b/tests/Unit/FutureTest.php @@ -4,9 +4,11 @@ namespace Redmine\Tests\Unit; +use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Redmine\Future; +#[CoversClass(Future::class)] final class FutureTest extends TestCase { public function testIsForwardCompatabilityEnabledReturnsFalseByDefault(): void From a7be46fac7f0482e97ca3d51728bb5b730dbd347 Mon Sep 17 00:00:00 2001 From: Art4 Date: Tue, 10 Mar 2026 21:48:47 +0100 Subject: [PATCH 5/5] feat: check for correct response status code on create issue --- src/Redmine/Api/Issue.php | 9 ++++-- tests/Unit/Api/Issue/CreateTest.php | 45 ++++++++++++++++++++++++----- tests/Unit/Api/IssueTest.php | 3 ++ 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/Redmine/Api/Issue.php b/src/Redmine/Api/Issue.php index 7319b364..eb481235 100644 --- a/src/Redmine/Api/Issue.php +++ b/src/Redmine/Api/Issue.php @@ -8,6 +8,7 @@ use Redmine\Exception; use Redmine\Exception\SerializerException; use Redmine\Exception\UnexpectedResponseException; +use Redmine\Future; use Redmine\Http\HttpClient; use Redmine\Http\HttpFactory; use Redmine\Serializer\JsonSerializer; @@ -254,8 +255,12 @@ public function create(array $params = []) $body = $this->lastResponse->getContent(); - if ($body === '') { - return $body; + if ($this->lastResponse->getStatusCode() !== 201) { + if (!Future::isForwardCompatibilityEnabled() && $body === '') { + return $body; + } + + throw UnexpectedResponseException::create($this->lastResponse); } return new SimpleXMLElement($body); diff --git a/tests/Unit/Api/Issue/CreateTest.php b/tests/Unit/Api/Issue/CreateTest.php index dd4e8030..b4fd2b41 100644 --- a/tests/Unit/Api/Issue/CreateTest.php +++ b/tests/Unit/Api/Issue/CreateTest.php @@ -6,6 +6,8 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Redmine\Api\Issue; +use Redmine\Exception\UnexpectedResponseException; +use Redmine\Future; use Redmine\Tests\Fixtures\AssertingHttpClient; use SimpleXMLElement; @@ -237,7 +239,7 @@ public static function getCreateData(): array ]; } - public function testCreateReturnsEmptyString(): void + public function testCreateWithIncorrectStatusCodeReturnsEmptyString(): void { $client = AssertingHttpClient::create( $this, @@ -261,6 +263,35 @@ public function testCreateReturnsEmptyString(): void $this->assertSame('', $return); } + public function testCreateWithIncorrectStatusCodeThrowsException(): void + { + $client = AssertingHttpClient::create( + $this, + [ + 'POST', + '/issues.xml', + 'application/xml', + '', + 500, + '', + '', + ], + ); + + // Create the object under test + $api = Issue::fromHttpClient($client); + + $this->expectException(UnexpectedResponseException::class); + + try { + Future::enableForwardCompatibility(); + + $api->create([]); + } finally { + Future::disableForwardCompatibility(); + } + } + public function testCreateWithHttpClientRetrievesIssueStatusId(): void { $client = AssertingHttpClient::create( @@ -279,7 +310,7 @@ public function testCreateWithHttpClientRetrievesIssueStatusId(): void '/issues.xml', 'application/xml', '123', - 200, + 201, 'application/xml', '', ], @@ -316,7 +347,7 @@ public function testCreateWithHttpClientRetrievesProjectId(): void '/issues.xml', 'application/xml', '3', - 200, + 201, 'application/xml', '', ], @@ -353,7 +384,7 @@ public function testCreateWithHttpClientRetrievesIssueCategoryId(): void '/issues.xml', 'application/xml', '345', - 200, + 201, 'application/xml', '', ], @@ -390,7 +421,7 @@ public function testCreateWithHttpClientRetrievesTrackerId(): void '/issues.xml', 'application/xml', '9', - 200, + 201, 'application/xml', '', ], @@ -427,7 +458,7 @@ public function testCreateWithHttpClientRetrievesUserId(): void '/issues.xml', 'application/xml', '65', - 200, + 201, 'application/xml', '', ], @@ -513,7 +544,7 @@ public function testCreateWithClientCleansParameters(): void 5 XML, - 200, + 201, 'application/xml', '', ], diff --git a/tests/Unit/Api/IssueTest.php b/tests/Unit/Api/IssueTest.php index 1e112f25..695e5b04 100644 --- a/tests/Unit/Api/IssueTest.php +++ b/tests/Unit/Api/IssueTest.php @@ -276,6 +276,9 @@ public function testCreateWithClientCleansParameters(): void $legacyClient->expects($this->exactly(1)) ->method('getLastResponseContentType') ->willReturn('application/xml'); + $legacyClient->expects($this->exactly(1)) + ->method('getLastResponseStatusCode') + ->willReturn(201); // Create the object under test $api = new Issue($legacyClient);