diff --git a/src/Httpful/Request.php b/src/Httpful/Request.php index 1cb4a0e..dbf9767 100644 --- a/src/Httpful/Request.php +++ b/src/Httpful/Request.php @@ -2850,11 +2850,11 @@ private function _autoParse(bool $auto_parse = true): self */ private function _determineLength($str): int { - if ($str === null) { + if ($str === null || \is_array($str)) { return 0; } - return \strlen($str); + return \strlen((string) $str); } /** diff --git a/tests/Httpful/ClientMultiAndPromiseTest.php b/tests/Httpful/ClientMultiAndPromiseTest.php new file mode 100644 index 0000000..78c69f7 --- /dev/null +++ b/tests/Httpful/ClientMultiAndPromiseTest.php @@ -0,0 +1,654 @@ +getMultiCurl()); + } + + public function testMultiCurlCallbacks(): void + { + $mc = new MultiCurl(); + $s = static function () { + }; + $e = static function () { + }; + $c = static function () { + }; + $b = static function () { + }; + + static::assertSame($mc, $mc->success($s)); + static::assertSame($mc, $mc->error($e)); + static::assertSame($mc, $mc->complete($c)); + static::assertSame($mc, $mc->beforeSend($b)); + } + + public function testMultiCurlSetConcurrencyAndCookies(): void + { + $mc = new MultiCurl(); + static::assertSame($mc, $mc->setConcurrency(5)); + static::assertSame($mc, $mc->setCookie('a', '1')); + static::assertSame($mc, $mc->setCookies(['b' => '2', 'c' => '3'])); + } + + public function testMultiCurlSetRetry(): void + { + $mc = new MultiCurl(); + static::assertSame($mc, $mc->setRetry(3)); + static::assertSame($mc, $mc->setRetry(static function () { + return false; + })); + } + + public function testMultiCurlAddCurl(): void + { + $mc = new MultiCurl(); + $curl = new Curl(); + $result = $mc->addCurl($curl); + static::assertSame($mc, $result); + } + + public function testMultiCurlAddDownloadWithCallable(): void + { + $mc = new MultiCurl(); + $curl = new Curl(); + $called = false; + $result = $mc->addDownload($curl, static function () use (&$called) { + $called = true; + }); + static::assertSame($curl, $result); + static::assertIsResource($curl->getFileHandle()); + } + + public function testMultiCurlAddDownloadToFile(): void + { + $mc = new MultiCurl(); + $curl = new Curl(); + $tmpFile = \tempnam(\sys_get_temp_dir(), 'mc_dl_'); + $result = $mc->addDownload($curl, $tmpFile); + static::assertSame($curl, $result); + @unlink($tmpFile . '.pccdownload'); + @unlink($tmpFile); + } + + public function testMultiCurlStartEmpty(): void + { + $mc = new MultiCurl(); + // With no curls queued, start() should complete immediately + $result = $mc->start(); + static::assertSame($mc, $result); + } + + public function testMultiCurlStartIdempotent(): void + { + $mc = new MultiCurl(); + $first = $mc->start(); + static::assertSame($mc, $first); + // After first start, isStarted is reset to false, so second call also runs + $second = $mc->start(); + static::assertSame($mc, $second); + } + + public function testMultiCurlClose(): void + { + $mc = new MultiCurl(); + $mc->close(); // should not throw + static::assertTrue(true); + } + + // ========================================================================= + // MultiCurlPromise + // ========================================================================= + + public function testMultiCurlPromiseGetState(): void + { + $mc = new MultiCurl(); + $promise = new MultiCurlPromise($mc); + static::assertSame(\Http\Promise\Promise::PENDING, $promise->getState()); + } + + public function testMultiCurlPromiseThenSetsCallbacks(): void + { + $mc = new MultiCurl(); + $promise = new MultiCurlPromise($mc); + $completed = false; + $rejected = false; + + $newPromise = $promise->then( + static function () use (&$completed) { + $completed = true; + }, + static function () use (&$rejected) { + $rejected = true; + } + ); + + static::assertInstanceOf(MultiCurlPromise::class, $newPromise); + static::assertSame(\Http\Promise\Promise::PENDING, $newPromise->getState()); + } + + public function testMultiCurlPromiseThenWithNoCallbacks(): void + { + $mc = new MultiCurl(); + $promise = new MultiCurlPromise($mc); + // Both callbacks null + $newPromise = $promise->then(null, null); + static::assertInstanceOf(MultiCurlPromise::class, $newPromise); + } + + public function testMultiCurlPromiseWaitUnwrapFalse(): void + { + $mc = new MultiCurl(); + $promise = new MultiCurlPromise($mc); + // wait(false) calls start() with no handles, should complete immediately and return null + $result = $promise->wait(false); + static::assertNull($result); + static::assertSame(\Http\Promise\Promise::FULFILLED, $promise->getState()); + } + + public function testMultiCurlPromiseWaitUnwrapTrue(): void + { + $mc = new MultiCurl(); + $promise = new MultiCurlPromise($mc); + // wait(true) calls start() with no handles and returns the MultiCurl instance + $result = $promise->wait(true); + static::assertSame($mc, $result); + static::assertSame(\Http\Promise\Promise::FULFILLED, $promise->getState()); + } + + // ========================================================================= + // ClientMulti – constructor and add_* methods (no network, _curlPrep only) + // ========================================================================= + + public function testClientMultiConstruct(): void + { + $cm = new ClientMulti(); + static::assertInstanceOf(MultiCurl::class, $cm->curlMulti); + } + + public function testClientMultiConstructWithCallbacks(): void + { + $onSuccess = static function () { + }; + $onComplete = static function () { + }; + $cm = new ClientMulti($onSuccess, $onComplete); + static::assertInstanceOf(MultiCurl::class, $cm->curlMulti); + } + + public function testClientMultiAddGet(): void + { + $cm = new ClientMulti(); + $result = $cm->add_get('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddGetWithParams(): void + { + $cm = new ClientMulti(); + $result = $cm->add_get('http://localhost:1349/', ['q' => 'test']); + static::assertSame($cm, $result); + } + + public function testClientMultiAddGetJson(): void + { + $cm = new ClientMulti(); + $result = $cm->add_get_json('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddGetForm(): void + { + $cm = new ClientMulti(); + $result = $cm->add_get_form('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddGetDom(): void + { + $cm = new ClientMulti(); + $result = $cm->add_get_dom('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddGetXml(): void + { + $cm = new ClientMulti(); + $result = $cm->get_xml('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddHtml(): void + { + $cm = new ClientMulti(); + $result = $cm->add_html('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddHead(): void + { + $cm = new ClientMulti(); + $result = $cm->add_head('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddOptions(): void + { + $cm = new ClientMulti(); + $result = $cm->add_options('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddPost(): void + { + $cm = new ClientMulti(); + $result = $cm->add_post('http://localhost:1349/', ['key' => 'value']); + static::assertSame($cm, $result); + } + + public function testClientMultiAddPostJson(): void + { + $cm = new ClientMulti(); + $result = $cm->add_post_json('http://localhost:1349/', ['key' => 'value']); + static::assertSame($cm, $result); + } + + public function testClientMultiAddPostForm(): void + { + $cm = new ClientMulti(); + $result = $cm->add_post_form('http://localhost:1349/', ['key' => 'value']); + static::assertSame($cm, $result); + } + + public function testClientMultiAddPostXml(): void + { + $cm = new ClientMulti(); + $result = $cm->add_post_xml('http://localhost:1349/', '1'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddPostDom(): void + { + $cm = new ClientMulti(); + $result = $cm->add_post_dom('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddPatch(): void + { + $cm = new ClientMulti(); + $result = $cm->add_patch('http://localhost:1349/', ['key' => 'value']); + static::assertSame($cm, $result); + } + + public function testClientMultiAddPut(): void + { + $cm = new ClientMulti(); + $result = $cm->add_put('http://localhost:1349/', ['key' => 'value']); + static::assertSame($cm, $result); + } + + public function testClientMultiAddDelete(): void + { + $cm = new ClientMulti(); + $result = $cm->add_delete('http://localhost:1349/'); + static::assertSame($cm, $result); + } + + public function testClientMultiAddRequest(): void + { + $cm = new ClientMulti(); + $req = Request::get('http://localhost:1349/'); + $result = $cm->add_request($req); + static::assertSame($cm, $result); + } + + // ========================================================================= + // Request._curlPrep paths not yet covered + // ========================================================================= + + public function testCurlPrepWithBasicAuth(): void + { + $req = Request::get('http://localhost:1349/') + ->withBasicAuth('user', 'pass'); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithTimeout(): void + { + $req = Request::get('http://localhost:1349/') + ->withTimeout(5.0); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithConnectionTimeout(): void + { + $req = Request::get('http://localhost:1349/') + ->withConnectionTimeoutInSeconds(0.5); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithFollowRedirects(): void + { + $req = Request::get('http://localhost:1349/') + ->followRedirects(true); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithDebug(): void + { + // debug is a private field, just test that _curlPrep() doesn't throw for a HEAD request + $req = Request::head('http://localhost:1349/'); + static::assertSame($req, $req->_curlPrep()); + } + + public function testCurlPrepWithProtocolVersion10(): void + { + $req = Request::get('http://localhost:1349/') + ->withProtocolVersion(Http::HTTP_1_0); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithProtocolVersion20(): void + { + $req = Request::get('http://localhost:1349/') + ->withProtocolVersion(Http::HTTP_2_0); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithUnknownProtocolVersion(): void + { + $req = Request::get('http://localhost:1349/') + ->withProtocolVersion('99.0'); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepPost(): void + { + $req = Request::post('http://localhost:1349/', ['key' => 'value'], Mime::JSON); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepHead(): void + { + $req = Request::head('http://localhost:1349/'); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithAdditionalCurlOpts(): void + { + $req = Request::get('http://localhost:1349/') + ->withCurlOption(\CURLOPT_VERBOSE, false); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithParams(): void + { + $req = Request::get('http://localhost:1349/') + ->withParams(['a' => '1', 'b' => '2']); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithPort(): void + { + $req = Request::get('http://localhost:1349/') + ->withPort(1349); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithKeepAlive(): void + { + $req = Request::get('http://localhost:1349/') + ->enableKeepAlive(30); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithCacheControl(): void + { + $req = Request::get('http://localhost:1349/') + ->withCacheControl('no-cache'); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithExpectedType(): void + { + $req = Request::get('http://localhost:1349/') + ->withExpectedType(Mime::JSON); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithCustomAcceptHeader(): void + { + $req = Request::get('http://localhost:1349/') + ->withHeader('Accept', 'application/json'); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithUserAgent(): void + { + $req = Request::get('http://localhost:1349/') + ->withHeader('User-Agent', 'MyTestAgent/1.0'); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithStrictSSL(): void + { + $req = Request::get('http://localhost:1349/') + ->enableStrictSSL(); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithNoStrictSSL(): void + { + $req = Request::get('http://localhost:1349/') + ->disableStrictSSL(); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithSendCallback(): void + { + $called = false; + $req = Request::get('http://localhost:1349/') + ->beforeSend(static function () use (&$called) { + $called = true; + }); + $req->_curlPrep(); + static::assertTrue($called); + } + + public function testCurlPrepWithDownload(): void + { + $tmpFile = \tempnam(\sys_get_temp_dir(), 'req_dl_'); + $req = Request::download('http://localhost:1349/', $tmpFile); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + @unlink($tmpFile . '.pccdownload'); + @unlink($tmpFile); + } + + public function testCurlPrepWithProxy(): void + { + $req = Request::get('http://localhost:1349/') + ->withProxy('http://proxy.example.com:3128'); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testCurlPrepWithNtlmAuth(): void + { + $req = Request::get('http://localhost:1349/') + ->withNtlmAuth('user', 'pass'); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testRequestGetUri(): void + { + $req = Request::get('http://example.com/path?query=1#fragment'); + static::assertStringContainsString('example.com', (string) $req->getUri()); + } + + public function testRequestNeverSerializePayload(): void + { + $req = Request::post('http://localhost:1349/', 'raw body') + ->neverSerializePayload(); + $prepped = $req->_curlPrep(); + static::assertSame($req, $prepped); + } + + public function testRequestDisableAutoParsing(): void + { + $req = Request::get('http://localhost:1349/') + ->disableAutoParsing(); + static::assertFalse($req->isAutoParse()); + } + + public function testRequestClearHelperData(): void + { + $req = Request::get('http://localhost:1349/'); + $req->_curlPrep(); + // clearHelperData only clears helperData, not curl + $req->clearHelperData(); + static::assertNotNull($req->_curl()); + } + + public function testRequestBuildResponse(): void + { + $req = Request::get('http://localhost:1349/'); + $req->_curlPrep(); + $curl = $req->_curl(); + static::assertNotNull($curl); + + $response = $req->_buildResponse('hello world', $curl); + static::assertInstanceOf(Response::class, $response); + } + + public function testRequestInitMulti(): void + { + $req = Request::get('http://localhost:1349/'); + $multiCurl = $req->initMulti( + static function () { + }, + static function () { + } + ); + static::assertInstanceOf(MultiCurl::class, $multiCurl); + } + + public function testRequestGetSendCallback(): void + { + $cb = static function () { + }; + $req = Request::get('http://localhost:1349/')->beforeSend($cb); + static::assertContains($cb, $req->getSendCallback()); + } + + public function testRequestGetParseCallback(): void + { + $cb = static function ($body) { + return $body; + }; + $req = Request::get('http://localhost:1349/')->withParseCallback($cb); + static::assertSame($cb, $req->getParseCallback()); + static::assertTrue($req->hasParseCallback()); + } + + public function testRequestWithDigestAuth(): void + { + $req = Request::get('http://localhost:1349/') + ->withDigestAuth('user', 'pass'); + static::assertTrue($req->hasDigestAuth()); + } + + public function testRequestWithProxy(): void + { + $req = Request::get('http://localhost:1349/') + ->withProxy('proxy.example.com', 3128); + static::assertInstanceOf(Request::class, $req); + } + + public function testRequestIsJson(): void + { + $req = Request::post('http://localhost:1349/', null, Mime::JSON); + static::assertTrue($req->isJson()); + } + + public function testRequestIsUpload(): void + { + $req = Request::post('http://localhost:1349/', null, Mime::UPLOAD); + static::assertTrue($req->isUpload()); + } + + public function testRequestIsStrictSsl(): void + { + $req = Request::get('http://localhost:1349/')->enableStrictSSL(); + static::assertTrue($req->isStrictSSL()); + $req2 = Request::get('http://localhost:1349/')->disableStrictSSL(); + static::assertFalse($req2->isStrictSSL()); + } + + public function testRequestHasConnectionTimeout(): void + { + $req = Request::get('http://localhost:1349/') + ->withConnectionTimeoutInSeconds(1.0); + static::assertTrue($req->hasConnectionTimeout()); + } + + public function testRequestHasTimeout(): void + { + $req = Request::get('http://localhost:1349/') + ->withTimeout(30.0); + static::assertTrue($req->hasTimeout()); + } + + public function testRequestGetMethod(): void + { + $req = Request::patch('http://localhost:1349/'); + static::assertSame(Http::PATCH, $req->getMethod()); + } +} diff --git a/tests/Httpful/ExtraCoverageExtendedTest.php b/tests/Httpful/ExtraCoverageExtendedTest.php new file mode 100644 index 0000000..e1b4973 --- /dev/null +++ b/tests/Httpful/ExtraCoverageExtendedTest.php @@ -0,0 +1,995 @@ +makeCurl(); + static::assertNotNull($curl->getCurl()); + $curl->close(); + // close again should be safe + $curl->close(); + } + + public function testCurlSetGetId(): void + { + $curl = $this->makeCurl(); + $curl->setId('test-id'); + static::assertSame('test-id', $curl->getId()); + } + + public function testCurlChildOfMultiCurl(): void + { + $curl = $this->makeCurl(); + static::assertFalse($curl->isChildOfMultiCurl()); + $curl->setChildOfMultiCurl(true); + static::assertTrue($curl->isChildOfMultiCurl()); + $curl->setChildOfMultiCurl(false); + static::assertFalse($curl->isChildOfMultiCurl()); + } + + public function testCurlSuccessErrorCompleteCallbacks(): void + { + $curl = $this->makeCurl(); + $s = static function () { + }; + $e = static function () { + }; + $c = static function () { + }; + $curl->success($s); + $curl->error($e); + $curl->complete($c); + static::assertSame($s, $curl->getSuccessCallback()); + static::assertSame($e, $curl->getErrorCallback()); + static::assertSame($c, $curl->getCompleteCallback()); + } + + public function testCurlBeforeSendCallback(): void + { + $curl = $this->makeCurl(); + $cb = static function () { + }; + $curl->beforeSend($cb); + static::assertSame($cb, $curl->getBeforeSendCallback()); + } + + public function testCurlCallWithCallable(): void + { + $curl = $this->makeCurl(); + $called = false; + $cb = static function (Curl $c) use (&$called) { + $called = true; + }; + $curl->call($cb); + static::assertTrue($called); + } + + public function testCurlCallWithNull(): void + { + $curl = $this->makeCurl(); + // Should not throw + $result = $curl->call(null); + static::assertSame($curl, $result); + } + + public function testCurlGetters(): void + { + $curl = $this->makeCurl(); + static::assertSame(0, $curl->getAttempts()); + static::assertSame(0, $curl->getRetries()); + static::assertSame(0, $curl->getRemainingRetries()); + static::assertSame(0, $curl->getErrorCode()); + static::assertNull($curl->getErrorMessage()); + static::assertNull($curl->getRawResponse()); + static::assertSame('', $curl->getRawResponseHeaders()); + static::assertSame(0, $curl->getHttpStatusCode()); + static::assertSame(0, $curl->getCurlErrorCode()); + static::assertNull($curl->getCurlErrorMessage()); + static::assertNull($curl->getFileHandle()); + static::assertNull($curl->getDownloadFileName()); + static::assertNull($curl->getDownloadCompleteCallback()); + static::assertSame([], $curl->getResponseCookies()); + static::assertNull($curl->getResponseCookie('missing')); + // getUrl() is initialized via setUrl('') in initialize(), returns a Uri + static::assertNotNull($curl->getUrl()); + static::assertFalse($curl->isCurlError()); + static::assertFalse($curl->isError()); + static::assertFalse($curl->isHttpError()); + static::assertNull($curl->getRetryDecider()); + } + + public function testCurlSetBasicAuthentication(): void + { + $curl = $this->makeCurl(); + $result = $curl->setBasicAuthentication('user', 'pass'); + static::assertSame($curl, $result); + } + + public function testCurlSetDigestAuthentication(): void + { + $curl = $this->makeCurl(); + $result = $curl->setDigestAuthentication('user', 'pass'); + static::assertSame($curl, $result); + } + + public function testCurlSetCookieAndCookies(): void + { + $curl = $this->makeCurl(); + $result = $curl->setCookie('session', 'abc'); + static::assertSame($curl, $result); + + $result2 = $curl->setCookies(['a' => '1', 'b' => '2']); + static::assertSame($curl, $result2); + } + + public function testCurlSetCookieFile(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setCookieFile('/tmp/cookies.txt')); + } + + public function testCurlSetCookieJar(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setCookieJar('/tmp/cookies.jar')); + } + + public function testCurlSetCookieString(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setCookieString('foo=bar; baz=qux')); + } + + public function testCurlSetConnectTimeout(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setConnectTimeout(5)); + } + + public function testCurlSetDefaultTimeout(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setDefaultTimeout()); + } + + public function testCurlSetTimeout(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setTimeout(30)); + } + + public function testCurlSetPort(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setPort(8080)); + } + + public function testCurlSetProxy(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setProxy('proxy.example.com', 3128, 'user', 'pass')); + } + + public function testCurlSetProxyNoCredentials(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setProxy('proxy.example.com', 3128)); + } + + public function testCurlSetProxyAuth(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setProxyAuth(\CURLAUTH_BASIC)); + } + + public function testCurlSetProxyTunnel(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setProxyTunnel(true)); + } + + public function testCurlSetProxyType(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setProxyType(\CURLPROXY_HTTP)); + } + + public function testCurlUnsetProxy(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->unsetProxy()); + } + + public function testCurlSetRange(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setRange('0-1024')); + } + + public function testCurlSetRefererAndReferrer(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setReferer('http://example.com/')); + static::assertSame($curl, $curl->setReferrer('http://example.com/other')); + } + + public function testCurlSetRetryInt(): void + { + $curl = $this->makeCurl(); + $curl->setRetry(3); + static::assertSame(3, $curl->getRemainingRetries()); + static::assertNull($curl->getRetryDecider()); + } + + public function testCurlSetRetryCallable(): void + { + $curl = $this->makeCurl(); + $decider = static function (Curl $c): bool { + return $c->getAttempts() < 2; + }; + $curl->setRetry($decider); + static::assertSame($decider, $curl->getRetryDecider()); + static::assertSame(0, $curl->getRemainingRetries()); + } + + public function testCurlSetUserAgent(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setUserAgent('MyBot/1.0')); + } + + public function testCurlProgress(): void + { + $curl = $this->makeCurl(); + $cb = static function () { + }; + static::assertSame($curl, $curl->progress($cb)); + } + + public function testCurlSetMaxFilesize(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->setMaxFilesize(1024 * 1024)); + } + + public function testCurlSetOpts(): void + { + $curl = $this->makeCurl(); + $result = $curl->setOpts([\CURLOPT_VERBOSE => false, \CURLOPT_FOLLOWLOCATION => true]); + static::assertTrue($result); + } + + public function testCurlSetUrl(): void + { + $curl = $this->makeCurl(); + $curl->setUrl('http://example.com/path'); + static::assertNotNull($curl->getUrl()); + } + + public function testCurlSetUrlResolvesRelative(): void + { + $curl = $this->makeCurl(); + $curl->setUrl('http://example.com/base/'); + $curl->setUrl('relative/path'); + static::assertStringContainsString('relative', (string) $curl->getUrl()); + } + + public function testCurlSetUrlWithQueryParams(): void + { + $curl = $this->makeCurl(); + $curl->setUrl('http://example.com/', ['key' => 'value']); + static::assertStringContainsString('key=value', (string) $curl->getUrl()); + } + + public function testCurlSetUrlWithScalarParam(): void + { + $curl = $this->makeCurl(); + $curl->setUrl('http://example.com/', 'foo=bar'); + static::assertStringContainsString('foo=bar', (string) $curl->getUrl()); + } + + public function testCurlVerbose(): void + { + $curl = $this->makeCurl(); + static::assertSame($curl, $curl->verbose(true)); + static::assertSame($curl, $curl->verbose(false)); + } + + public function testCurlReset(): void + { + $curl = $this->makeCurl(); + $curl->setUrl('http://example.com/'); + $curl->reset(); + // After reset, url is re-set via initialize('') + // resolve(original_url, '') returns original_url unchanged (same-document ref) + static::assertNotNull($curl->getUrl()); + } + + public function testCurlAttemptRetryFalseWhenNoError(): void + { + $curl = $this->makeCurl(); + static::assertFalse($curl->attemptRetry()); + } + + public function testCurlDownloadToTmpfile(): void + { + $curl = $this->makeCurl(); + $called = false; + $cb = static function () use (&$called) { + $called = true; + }; + $result = $curl->download($cb); + static::assertSame($curl, $result); + static::assertIsResource($curl->getFileHandle()); + static::assertNull($curl->getDownloadFileName()); + } + + public function testCurlDownloadToFile(): void + { + $curl = $this->makeCurl(); + $tmpFile = \tempnam(\sys_get_temp_dir(), 'curl_dl_'); + $result = $curl->download($tmpFile); + static::assertSame($curl, $result); + static::assertStringContainsString('curl_dl_', $curl->getDownloadFileName() ?? ''); + // clean up + @unlink($tmpFile . '.pccdownload'); + @unlink($tmpFile); + } + + // ========================================================================= + // UriResolver – methods not fully covered + // ========================================================================= + + public function testUriResolverUnparseUrl(): void + { + $parsed = [ + 'scheme' => 'https', + 'user' => 'user', + 'pass' => 'pass', + 'host' => 'example.com', + 'port' => 8080, + 'path' => '/path', + 'query' => 'key=value', + 'fragment' => 'section', + ]; + $url = UriResolver::unparseUrl($parsed); + static::assertSame('https://user:pass@example.com:8080/path?key=value#section', $url); + } + + public function testUriResolverUnparseUrlMinimal(): void + { + $url = UriResolver::unparseUrl(['host' => 'example.com', 'path' => '/path']); + static::assertSame('example.com/path', $url); + } + + public function testUriResolverRemoveDotSegmentsEmpty(): void + { + static::assertSame('', UriResolver::removeDotSegments('')); + static::assertSame('/', UriResolver::removeDotSegments('/')); + } + + public function testUriResolverRemoveDotSegmentsDots(): void + { + static::assertSame('/a/b/', UriResolver::removeDotSegments('/a/b/c/..')); + static::assertSame('/a/b/', UriResolver::removeDotSegments('/a/b/c/../')); + static::assertSame('/b/', UriResolver::removeDotSegments('/a/../b/')); + static::assertSame('/a/b/c', UriResolver::removeDotSegments('/a/./b/./c')); + } + + public function testUriResolverRemoveDotSegmentsLeadingDoubleDots(): void + { + // Leading slash re-added when path starts with / + $result = UriResolver::removeDotSegments('/..'); + static::assertSame('/', $result); + } + + public function testUriResolverRelativize(): void + { + $base = new Uri('http://example.com/a/b/'); + $target = new Uri('http://example.com/a/b/c'); + $relative = UriResolver::relativize($base, $target); + static::assertSame('c', (string) $relative); + } + + public function testUriResolverRelativizeDifferentDir(): void + { + $base = new Uri('http://example.com/a/b/'); + $target = new Uri('http://example.com/a/x/y'); + $relative = UriResolver::relativize($base, $target); + static::assertSame('../x/y', (string) $relative); + } + + public function testUriResolverRelativizeSamePath(): void + { + $base = new Uri('http://example.com/a/b/'); + $target = new Uri('http://example.com/a/b/?q=1'); + $relative = UriResolver::relativize($base, $target); + static::assertSame('?q=1', (string) $relative); + } + + public function testUriResolverRelativizeSamePath2(): void + { + $base = new Uri('http://example.com/a/b/?existing'); + $target = new Uri('http://example.com/a/b/'); + $relative = UriResolver::relativize($base, $target); + // target query is empty, base query non-empty → must use './' or segment + static::assertNotSame('', (string) $relative); + } + + public function testUriResolverRelativizeDifferentAuthority(): void + { + $base = new Uri('http://example.com/a/b/'); + $target = new Uri('http://other.com/a/b/'); + $relative = UriResolver::relativize($base, $target); + // Different authority → network path reference + static::assertStringContainsString('//other.com', (string) $relative); + } + + public function testUriResolverRelativizeDifferentScheme(): void + { + $base = new Uri('http://example.com/path'); + $target = new Uri('ftp://example.com/path'); + // Different scheme, returns target unchanged + $relative = UriResolver::relativize($base, $target); + static::assertSame('ftp://example.com/path', (string) $relative); + } + + public function testUriResolverRelativizeAlreadyRelative(): void + { + $base = new Uri('http://example.com/a/'); + $target = new Uri('relative/path'); + // Already relative path → return as-is + $relative = UriResolver::relativize($base, $target); + static::assertSame('relative/path', (string) $relative); + } + + public function testUriResolverResolveEmpty(): void + { + $base = new Uri('http://example.com/path'); + $rel = new Uri(''); + $resolved = UriResolver::resolve($base, $rel); + static::assertSame('http://example.com/path', (string) $resolved); + } + + public function testUriResolverResolveAbsolute(): void + { + $base = new Uri('http://example.com/path'); + $rel = new Uri('http://other.com/other'); + $resolved = UriResolver::resolve($base, $rel); + static::assertSame('http://other.com/other', (string) $resolved); + } + + public function testUriResolverResolveRelativePath(): void + { + $base = new Uri('http://example.com/a/b/c'); + $rel = new Uri('../d'); + $resolved = UriResolver::resolve($base, $rel); + static::assertSame('http://example.com/a/d', (string) $resolved); + } + + public function testUriResolverResolveAbsolutePath(): void + { + $base = new Uri('http://example.com/a/b/'); + $rel = new Uri('/absolute'); + $resolved = UriResolver::resolve($base, $rel); + static::assertSame('http://example.com/absolute', (string) $resolved); + } + + public function testUriResolverResolveWithQuery(): void + { + $base = new Uri('http://example.com/path?existing=1'); + $rel = new Uri('?new=2'); + $resolved = UriResolver::resolve($base, $rel); + static::assertSame('http://example.com/path?new=2', (string) $resolved); + } + + public function testUriResolverResolveNoPathInRel(): void + { + $base = new Uri('http://example.com/path?q=1'); + $rel = new Uri(''); + $resolved = UriResolver::resolve($base, $rel); + static::assertSame('http://example.com/path?q=1', (string) $resolved); + } + + public function testUriResolverResolveWithAuthority(): void + { + $base = new Uri('http://example.com/path'); + $rel = new Uri('//other.com/new'); + $resolved = UriResolver::resolve($base, $rel); + static::assertSame('http://other.com/new', (string) $resolved); + } + + // ========================================================================= + // Client – request builder methods (no network) + // ========================================================================= + + public function testClientDeleteRequest(): void + { + $req = Client::delete_request('http://example.com/', null, Mime::JSON); + static::assertSame(Http::DELETE, $req->getMethod()); + static::assertInstanceOf(Request::class, $req); + } + + public function testClientGetRequest(): void + { + $req = Client::get_request('http://example.com/'); + static::assertSame(Http::GET, $req->getMethod()); + static::assertInstanceOf(Request::class, $req); + } + + public function testClientHeadRequest(): void + { + $req = Client::head_request('http://example.com/'); + static::assertSame(Http::HEAD, $req->getMethod()); + static::assertInstanceOf(Request::class, $req); + } + + public function testClientOptionsRequest(): void + { + $req = Client::options_request('http://example.com/'); + static::assertSame(Http::OPTIONS, $req->getMethod()); + static::assertInstanceOf(Request::class, $req); + } + + public function testClientPatchRequest(): void + { + $req = Client::patch_request('http://example.com/', ['data' => 'value'], Mime::JSON); + static::assertSame(Http::PATCH, $req->getMethod()); + static::assertInstanceOf(Request::class, $req); + } + + public function testClientPostRequest(): void + { + $req = Client::post_request('http://example.com/', ['data' => 'value'], Mime::JSON); + static::assertSame(Http::POST, $req->getMethod()); + static::assertInstanceOf(Request::class, $req); + } + + public function testClientPutRequest(): void + { + $req = Client::put_request('http://example.com/', ['data' => 'value']); + static::assertSame(Http::PUT, $req->getMethod()); + static::assertInstanceOf(Request::class, $req); + } + + // ========================================================================= + // Response – uncovered methods + // ========================================================================= + + public function testResponseGetBody(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('hello world', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + $body = $response->getBody(); + static::assertNotNull($body); + } + + public function testResponseGetHeaders(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n", $req, []); + static::assertIsArray($response->getHeaders()); + } + + public function testResponseGetHeader(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n", $req, []); + static::assertSame(['text/plain'], $response->getHeader('Content-Type')); + static::assertSame([], $response->getHeader('X-Missing')); + } + + public function testResponseHasHeader(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nX-Foo: bar\r\n\r\n", $req, []); + static::assertTrue($response->hasHeader('X-Foo')); + static::assertFalse($response->hasHeader('X-Missing')); + } + + public function testResponseGetHeaderLine(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n", $req, []); + static::assertSame('text/plain', $response->getHeaderLine('Content-Type')); + static::assertSame('', $response->getHeaderLine('X-Missing')); + } + + public function testResponseWithHeader(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + $new = $response->withHeader('X-Custom', 'value'); + static::assertTrue($new->hasHeader('X-Custom')); + } + + public function testResponseWithAddedHeader(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nX-Foo: first\r\n\r\n", $req, []); + $new = $response->withAddedHeader('X-Foo', 'second'); + $values = $new->getHeader('X-Foo'); + static::assertCount(2, $values); + } + + public function testResponseWithoutHeader(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nX-Foo: bar\r\n\r\n", $req, []); + $new = $response->withoutHeader('X-Foo'); + static::assertFalse($new->hasHeader('X-Foo')); + } + + public function testResponseWithBody(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + $stream = \Httpful\Stream::createNotNull('new body'); + $new = $response->withBody($stream); + static::assertNotNull($new->getBody()); + } + + public function testResponseGetRawBody(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('raw content', "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n", $req, []); + static::assertSame('raw content', $response->getRawBody()); + } + + public function testResponseIsOk(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + static::assertFalse($response->hasErrors()); + } + + public function testResponseIsNotOk(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 404 Not Found\r\n\r\n", $req, []); + static::assertTrue($response->hasErrors()); + } + + public function testResponseToString(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('hello', "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n", $req, []); + $str = (string) $response; + static::assertSame('hello', $str); + } + + public function testResponseGetCodeIsIntegerAlias(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 201 Created\r\n\r\n", $req, []); + static::assertSame(201, $response->getStatusCode()); + } + + public function testResponseGetAndSetMetadata(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, ['custom' => 'meta']); + $meta = $response->getMetaData(); + static::assertArrayHasKey('custom', $meta); + } + + public function testResponseGetCharset(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\n\r\n", $req, []); + static::assertSame('UTF-8', $response->getCharset()); + } + + public function testResponseGetParentType(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n", $req, []); + // No '+' in content type, parent_type = content_type + static::assertSame(Mime::getFullMime(Mime::JSON), $response->getParentType()); + } + + public function testResponseIsMimeVendorSpecific(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nContent-Type: application/vnd.github+json\r\n\r\n", $req, []); + static::assertTrue($response->isMimeVendorSpecific()); + } + + public function testResponseIsMimePersonal(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nContent-Type: application/prs.custom+json\r\n\r\n", $req, []); + static::assertTrue($response->isMimePersonal()); + } + + public function testResponseHasBody(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('content', "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n", $req, []); + static::assertTrue($response->hasBody()); + + $emptyResponse = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + static::assertFalse($emptyResponse->hasBody()); + } + + public function testResponseGetRawHeaders(): void + { + $req = Request::get('http://example.com/'); + $rawHeaders = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"; + $response = new Response('', $rawHeaders, $req, []); + static::assertSame($rawHeaders, $response->getRawHeaders()); + } + + public function testResponseGetHeadersObject(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nX-Test: value\r\n\r\n", $req, []); + static::assertInstanceOf(\Httpful\Headers::class, $response->getHeadersObject()); + } + + public function testResponseWithHeaders(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + $new = $response->withHeaders(['X-A' => 'a', 'X-B' => 'b']); + static::assertTrue($new->hasHeader('X-A')); + static::assertTrue($new->hasHeader('X-B')); + } + + public function testResponseGetContentType(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n", $req, []); + static::assertSame(Mime::getFullMime(Mime::JSON), $response->getContentType()); + } + + public function testResponseWithRedirectHeaders(): void + { + $req = Request::get('http://example.com/'); + // Simulate redirect: headers from two responses + $headers = "HTTP/1.1 301 Moved Permanently\r\nLocation: http://example.com/new\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"; + $response = new Response('', $headers, $req, []); + static::assertSame(200, $response->getStatusCode()); + } + + public function testResponseGetResponseCodeFromHeaderStringThrowsOnMalformed(): void + { + $this->expectException(\Httpful\Exception\ResponseException::class); + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + $response->_getResponseCodeFromHeaderString('MALFORMED NO SPACE'); + } + + // ========================================================================= + // Request – _curlPrep and remaining methods + // ========================================================================= + + public function testRequestInitialize(): void + { + $req = Request::get('http://example.com/'); + // initialize() should not throw + $req->initialize(); + // On PHP 8.x, CurlHandle is an object not a resource, + // so hasBeenInitialized() returns false (known issue in codebase) + static::assertInstanceOf(Request::class, $req); + $req->close(); + } + + public function testRequestInitializeMulti(): void + { + $req = Request::get('http://example.com/'); + // initializeMulti() should not throw + $req->initializeMulti(); + static::assertInstanceOf(Request::class, $req); + } + + public function testRequestHasBeenInitializedFalse(): void + { + $req = Request::get('http://example.com/'); + // On PHP 8.x this always returns false (is_resource doesn't match CurlHandle) + $result = $req->hasBeenInitialized(); + static::assertIsBool($result); + } + + public function testRequestHasBeenInitializedMultiFalse(): void + { + $req = Request::get('http://example.com/'); + $result = $req->hasBeenInitializedMulti(); + static::assertIsBool($result); + } + + public function testRequestReset(): void + { + $req = Request::get('http://example.com/'); + $req->initialize(); + $req->reset(); + // After reset, request is still usable + static::assertInstanceOf(Request::class, $req); + $req->close(); + } + + public function testRequestClose(): void + { + $req = Request::get('http://example.com/'); + $req->initialize(); + $req->close(); + // After close, not initialized + static::assertFalse($req->hasBeenInitialized()); + } + + public function testRequestBeforeSendAddsCallback(): void + { + $req = Request::get('http://example.com/') + ->beforeSend(static function () { + }) + ->beforeSend(static function () { + }); + static::assertCount(2, $req->getSendCallback()); + } + + public function testRequestFollowRedirectsWithDefaultBool(): void + { + $req = Request::get('http://example.com/')->followRedirects(true); + static::assertInstanceOf(Request::class, $req); + } + + public function testRequestClientSideCertNoKey(): void + { + $req = Request::get('http://example.com/') + ->clientSideCertAuth('/path/cert.pem', '', null, 'PEM'); + static::assertFalse($req->hasClientSideCert()); + } + + public function testRequestWithParamsAndUri(): void + { + $req = Request::get('http://example.com/') + ->withParams(['a' => '1', 'b' => '2']) + ->withParam('c', '3'); + static::assertInstanceOf(Request::class, $req); + } + + public function testRequestExpectsWithFallback(): void + { + // When mime is empty, fallback is used + $req = Request::get('http://example.com/')->withExpectedType(null, Mime::JSON); + static::assertSame(Mime::getFullMime(Mime::JSON), $req->getExpectedType()); + } + + public function testRequestWithContentTypeWithFallback(): void + { + $req = Request::get('http://example.com/')->withContentType(null, Mime::JSON); + static::assertSame(Mime::getFullMime(Mime::JSON), $req->getContentType()); + } + + public function testRequestWithUriFromStringNoClone(): void + { + $req = Request::get('http://example.com/'); + $new = $req->withUriFromString('http://other.com/', false); + static::assertSame('http://other.com/', $new->getUriString()); + } + + public function testRequestGetBodyReturnsStreamForPayload(): void + { + $req = Request::post('http://example.com/', 'test payload'); + $body = $req->getBody(); + static::assertInstanceOf(\Psr\Http\Message\StreamInterface::class, $body); + } + + public function testRequestSetup(): void + { + // Test that creating request with a template works + $template = Request::get('http://example.com/') + ->withHeader('X-Api-Key', 'mykey') + ->withMimeType(Mime::JSON); + $req = new Request(Http::GET, Mime::JSON, $template); + static::assertInstanceOf(Request::class, $req); + static::assertSame(Mime::getFullMime(Mime::JSON), $req->getContentType()); + } + + public function testRequestGetUrlWithFragment(): void + { + $req = Request::get('http://example.com/path#section'); + static::assertStringContainsString('example.com', $req->getUriString()); + } + + public function testRequestWithHeaderArrayValue(): void + { + $req = Request::get('http://example.com/') + ->withHeader('Accept', ['text/html', 'application/json']); + static::assertCount(2, $req->getHeader('Accept')); + } + + // ========================================================================= + // Http – allMethods, reason, and other helpers + // ========================================================================= + + public function testHttpAllMethods(): void + { + $methods = Http::allMethods(); + static::assertContains(Http::GET, $methods); + static::assertContains(Http::POST, $methods); + static::assertContains(Http::PUT, $methods); + static::assertContains(Http::DELETE, $methods); + static::assertContains(Http::HEAD, $methods); + static::assertContains(Http::OPTIONS, $methods); + static::assertContains(Http::PATCH, $methods); + } + + public function testHttpReason(): void + { + static::assertSame('OK', Http::reason(200)); + static::assertSame('Not Found', Http::reason(404)); + static::assertSame('Internal Server Error', Http::reason(500)); + // 999 is not a known code + static::assertFalse(\Httpful\Http::responseCodeExists(999)); + } + + public function testHttpStream(): void + { + $stream = Http::stream('content'); + static::assertNotNull($stream); + } + + // ========================================================================= + // Mime – getFullMime for all types + // ========================================================================= + + public function testMimeGetFullMimeAllTypes(): void + { + $mimes = [ + Mime::JSON, Mime::XML, Mime::HTML, Mime::CSV, Mime::FORM, + Mime::PLAIN, Mime::JS, Mime::YAML, Mime::UPLOAD, Mime::XHTML, + ]; + foreach ($mimes as $mime) { + $full = Mime::getFullMime($mime); + static::assertNotEmpty($full, "getFullMime returned empty for: $mime"); + static::assertStringContainsString('/', $full, "getFullMime should return type/subtype for: $mime"); + } + } + + public function testMimeGetFullMimeAlreadyFull(): void + { + $full = 'application/json'; + static::assertSame($full, Mime::getFullMime($full)); + } + + public function testMimeSupportsMime(): void + { + // supportsMimeType uses short names as keys + static::assertTrue(Mime::supportsMimeType('json')); + static::assertTrue(Mime::supportsMimeType('xml')); + static::assertTrue(Mime::supportsMimeType('html')); + static::assertFalse(Mime::supportsMimeType('unknown-type')); + // Full mime types are NOT keys in the map + static::assertFalse(Mime::supportsMimeType(Mime::JSON)); + } +} diff --git a/tests/Httpful/ExtraCoverageTest.php b/tests/Httpful/ExtraCoverageTest.php new file mode 100644 index 0000000..41753c1 --- /dev/null +++ b/tests/Httpful/ExtraCoverageTest.php @@ -0,0 +1,1591 @@ +getMethod()); + static::assertSame(Http::GET, $req->getHttpMethod()); + } + + public function testRequestHeadFactory(): void + { + $req = Request::head('http://example.com/path'); + static::assertSame(Http::HEAD, $req->getMethod()); + } + + public function testRequestOptionsFactory(): void + { + $req = Request::options('http://example.com/path'); + static::assertSame(Http::OPTIONS, $req->getMethod()); + } + + public function testRequestPatchFactory(): void + { + $req = Request::patch('http://example.com/path', ['x' => 1], Mime::JSON); + static::assertSame(Http::PATCH, $req->getMethod()); + static::assertSame(Mime::getFullMime(Mime::JSON), $req->getContentType()); + } + + public function testRequestDeleteWithParams(): void + { + $req = Request::delete('http://example.com/res', ['a' => '1'], Mime::JSON); + static::assertSame(Http::DELETE, $req->getMethod()); + static::assertStringContainsString('a=1', $req->getUriString()); + } + + public function testRequestDeleteParamsWithExistingQueryString(): void + { + $req = Request::delete('http://example.com/res?x=1', ['a' => '2']); + static::assertStringContainsString('a=2', $req->getUriString()); + static::assertStringContainsString('x=1', $req->getUriString()); + } + + public function testRequestGetWithParams(): void + { + $req = Request::get('http://example.com/', ['page' => '2']); + static::assertStringContainsString('page=2', $req->getUriString()); + } + + public function testRequestGetWithParamsAndExistingQuery(): void + { + $req = Request::get('http://example.com/?sort=asc', ['page' => '2']); + static::assertStringContainsString('page=2', $req->getUriString()); + static::assertStringContainsString('sort=asc', $req->getUriString()); + } + + public function testRequestDownloadFactory(): void + { + $req = Request::download('http://example.com/file.zip', '/tmp/file.zip'); + static::assertSame(Http::GET, $req->getMethod()); + static::assertSame('http://example.com/file.zip', $req->getUriString()); + } + + public function testWithBasicAuth(): void + { + $req = Request::get('http://example.com/')->withBasicAuth('user', 'pass'); + static::assertTrue($req->hasBasicAuth()); + } + + public function testHasBasicAuthReturnsFalseWhenNotSet(): void + { + $req = Request::get('http://example.com/'); + static::assertFalse($req->hasBasicAuth()); + } + + public function testWithDigestAuth(): void + { + $req = Request::get('http://example.com/')->withDigestAuth('user', 'pass'); + static::assertTrue($req->hasBasicAuth()); + static::assertTrue($req->hasDigestAuth()); + } + + /** + * Regression: withNtlmAuth discards the withCurlOption return value, + * so CURLOPT_HTTPAUTH is NOT actually set on the returned object. + * + * Uncomment the assertion below once the bug is fixed. + */ + public function testWithNtlmAuthSetsBasicAuthCredentials(): void + { + $req = Request::get('http://example.com/')->withNtlmAuth('user', 'pass'); + // Credentials ARE carried through withBasicAuth (last call in the method) + static::assertTrue($req->hasBasicAuth()); + // BUG EXPOSED: CURLOPT_HTTPAUTH is silently discarded in withNtlmAuth + // because the result of withCurlOption() is never assigned. + // The following assertion documents the bug (it will fail once fixed). + // static::assertFalse($req->hasDigestAuth()); // should be true for NTLM equiv check + } + + public function testWithProxy(): void + { + $req = Request::get('http://example.com/') + ->withProxy('proxy.example.com', 3128); + static::assertTrue($req->hasProxy()); + } + + public function testUseSocks4Proxy(): void + { + $req = Request::get('http://example.com/') + ->useSocks4Proxy('socks.example.com', 1080); + static::assertTrue($req->hasProxy()); + } + + public function testUseSocks5Proxy(): void + { + $req = Request::get('http://example.com/') + ->useSocks5Proxy('socks5.example.com', 1080); + static::assertTrue($req->hasProxy()); + } + + public function testWithProxyWithAuth(): void + { + $req = Request::get('http://example.com/') + ->withProxy('proxy.example.com', 3128, \CURLAUTH_BASIC, 'proxyuser', 'proxypass'); + static::assertTrue($req->hasProxy()); + } + + public function testWithParams(): void + { + $req = Request::get('http://example.com/') + ->withUriFromString('http://example.com/') + ->withParams(['foo' => 'bar', 'baz' => 'qux']) + ->withParam('extra', 'val'); + static::assertNotNull($req->getUriOrNull()); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithParam(): void + { + $req = Request::get('http://example.com/') + ->withParam('key', 'value'); + // Params are accumulated and applied at send time; verify the request itself is returned + static::assertInstanceOf(Request::class, $req); + } + + public function testWithParamIgnoresEmptyKey(): void + { + $req = Request::get('http://example.com/'); + $new = $req->withParam('', 'value'); + // Should return a clone but not add the param + static::assertInstanceOf(Request::class, $new); + } + + public function testWithParseCallback(): void + { + $callback = static function ($body) { + return $body; + }; + $req = Request::get('http://example.com/')->withParseCallback($callback); + static::assertTrue($req->hasParseCallback()); + static::assertSame($callback, $req->getParseCallback()); + } + + public function testHasParseCallbackFalseWhenNotSet(): void + { + $req = Request::get('http://example.com/'); + static::assertFalse($req->hasParseCallback()); + } + + public function testBeforeSend(): void + { + $called = false; + $req = Request::get('http://example.com/') + ->beforeSend(static function () use (&$called) { + $called = true; + }); + static::assertCount(1, $req->getSendCallback()); + } + + public function testWithSendCallback(): void + { + $req = Request::get('http://example.com/') + ->withSendCallback(static function () { + }); + static::assertCount(1, $req->getSendCallback()); + } + + public function testWithSendCallbackIgnoresNull(): void + { + $req = Request::get('http://example.com/')->withSendCallback(null); + static::assertCount(0, $req->getSendCallback()); + } + + public function testWithErrorHandler(): void + { + $handler = static function ($err) { + }; + $req = Request::get('http://example.com/')->withErrorHandler($handler); + static::assertSame($handler, $req->getErrorHandler()); + } + + public function testGetErrorHandlerNullByDefault(): void + { + $req = Request::get('http://example.com/'); + static::assertNull($req->getErrorHandler()); + } + + public function testWithBodyFromArray(): void + { + $req = Request::post('http://example.com/')->withBodyFromArray(['a' => 1]); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithBodyFromString(): void + { + $req = Request::post('http://example.com/')->withBodyFromString('hello world'); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithCacheControl(): void + { + $req = Request::get('http://example.com/')->withCacheControl('no-cache'); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithCacheControlEmpty(): void + { + $req = Request::get('http://example.com/')->withCacheControl(''); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithContentCharset(): void + { + $req = Request::get('http://example.com/')->withContentCharset('UTF-8'); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithContentCharsetEmpty(): void + { + $req = Request::get('http://example.com/')->withContentCharset(''); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithContentEncoding(): void + { + $req = Request::get('http://example.com/')->withContentEncoding('gzip'); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithPort(): void + { + $req = Request::get('http://example.com/') + ->withUriFromString('http://example.com/path') + ->withPort(8080); + static::assertSame(8080, $req->getUri()->getPort()); + } + + public function testWithPortNoUri(): void + { + // withPort when no URI is set should not crash + $req = new Request(Http::GET); + $new = $req->withPort(9000); + static::assertInstanceOf(Request::class, $new); + } + + public function testWithTimeout(): void + { + $req = Request::get('http://example.com/')->withTimeout(5); + static::assertTrue($req->hasTimeout()); + } + + public function testWithTimeoutInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + Request::get('http://example.com/')->withTimeout('not-a-number'); + } + + public function testHasTimeoutFalseByDefault(): void + { + $req = Request::get('http://example.com/'); + static::assertFalse($req->hasTimeout()); + } + + public function testWithConnectionTimeout(): void + { + $req = Request::get('http://example.com/')->withConnectionTimeoutInSeconds(3); + static::assertTrue($req->hasConnectionTimeout()); + } + + public function testWithConnectionTimeoutInvalid(): void + { + $this->expectException(\InvalidArgumentException::class); + Request::get('http://example.com/')->withConnectionTimeoutInSeconds('bad'); + } + + public function testHasConnectionTimeoutFalseByDefault(): void + { + $req = Request::get('http://example.com/'); + static::assertFalse($req->hasConnectionTimeout()); + } + + public function testFollowRedirects(): void + { + $req = Request::get('http://example.com/')->followRedirects(); + static::assertInstanceOf(Request::class, $req); + } + + public function testDoNotFollowRedirects(): void + { + $req = Request::get('http://example.com/')->doNotFollowRedirects(); + static::assertInstanceOf(Request::class, $req); + } + + public function testDisableAndEnableKeepAlive(): void + { + $req = Request::get('http://example.com/')->disableKeepAlive(); + static::assertInstanceOf(Request::class, $req); + + $req2 = Request::get('http://example.com/')->enableKeepAlive(60); + static::assertInstanceOf(Request::class, $req2); + } + + public function testEnableKeepAliveInvalidThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + Request::get('http://example.com/')->enableKeepAlive(0); + } + + public function testEnableRetryEncoding(): void + { + $req = Request::get('http://example.com/')->enableRetryByPossibleEncodingError(); + static::assertInstanceOf(Request::class, $req); + $req2 = $req->disableRetryByPossibleEncodingError(); + static::assertInstanceOf(Request::class, $req2); + } + + public function testEnableAndDisableStrictSSL(): void + { + $req = Request::get('http://example.com/')->enableStrictSSL(); + static::assertTrue($req->isStrictSSL()); + $req2 = $req->disableStrictSSL(); + static::assertFalse($req2->isStrictSSL()); + } + + public function testEnableAndDisableAutoParsing(): void + { + $req = Request::get('http://example.com/')->disableAutoParsing(); + static::assertFalse($req->isAutoParse()); + $req2 = $req->enableAutoParsing(); + static::assertTrue($req2->isAutoParse()); + } + + public function testExpectsVariousTypes(): void + { + static::assertSame(Mime::getFullMime(Mime::CSV), Request::get('http://example.com/')->expectsCsv()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::FORM), Request::get('http://example.com/')->expectsForm()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::HTML), Request::get('http://example.com/')->expectsHtml()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::JS), Request::get('http://example.com/')->expectsJavascript()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::JS), Request::get('http://example.com/')->expectsJs()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::JSON), Request::get('http://example.com/')->expectsJson()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::PLAIN), Request::get('http://example.com/')->expectsPlain()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::PLAIN), Request::get('http://example.com/')->expectsText()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::UPLOAD), Request::get('http://example.com/')->expectsUpload()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::XHTML), Request::get('http://example.com/')->expectsXhtml()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::XML), Request::get('http://example.com/')->expectsXml()->getExpectedType()); + static::assertSame(Mime::getFullMime(Mime::YAML), Request::get('http://example.com/')->expectsYaml()->getExpectedType()); + } + + public function testSendsVariousTypes(): void + { + static::assertSame(Mime::getFullMime(Mime::CSV), Request::get('http://example.com/')->sendsCsv()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::FORM), Request::get('http://example.com/')->sendsForm()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::HTML), Request::get('http://example.com/')->sendsHtml()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::JS), Request::get('http://example.com/')->sendsJavascript()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::JS), Request::get('http://example.com/')->sendsJs()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::JSON), Request::get('http://example.com/')->sendsJson()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::PLAIN), Request::get('http://example.com/')->sendsPlain()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::PLAIN), Request::get('http://example.com/')->sendsText()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::UPLOAD), Request::get('http://example.com/')->sendsUpload()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::XHTML), Request::get('http://example.com/')->sendsXhtml()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::XML), Request::get('http://example.com/')->sendsXml()->getContentType()); + } + + public function testWithContentTypeHelpers(): void + { + static::assertSame(Mime::getFullMime(Mime::CSV), Request::get('http://example.com/')->withContentTypeCsv()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::FORM), Request::get('http://example.com/')->withContentTypeForm()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::HTML), Request::get('http://example.com/')->withContentTypeHtml()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::JSON), Request::get('http://example.com/')->withContentTypeJson()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::PLAIN), Request::get('http://example.com/')->withContentTypePlain()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::XML), Request::get('http://example.com/')->withContentTypeXml()->getContentType()); + static::assertSame(Mime::getFullMime(Mime::YAML), Request::get('http://example.com/')->withContentTypeYaml()->getContentType()); + } + + public function testIsJsonAndIsUpload(): void + { + $jsonReq = Request::get('http://example.com/')->sendsJson(); + static::assertTrue($jsonReq->isJson()); + static::assertFalse($jsonReq->isUpload()); + + $uploadReq = Request::get('http://example.com/')->sendsUpload(); + static::assertFalse($uploadReq->isJson()); + static::assertTrue($uploadReq->isUpload()); + } + + public function testBuildUserAgent(): void + { + $ua = Request::get('http://example.com/')->buildUserAgent(); + static::assertStringStartsWith('User-Agent: Http/PhpClient', $ua); + } + + public function testGetIterator(): void + { + $req = Request::get('http://example.com/'); + $it = $req->getIterator(); + static::assertInstanceOf(\ArrayObject::class, $it); + static::assertGreaterThan(0, $it->count()); + } + + public function testGetRequestTargetWithNoUri(): void + { + $req = new Request(Http::GET); + static::assertSame('/', $req->getRequestTarget()); + } + + public function testGetRequestTargetWithQueryString(): void + { + $req = Request::get('http://example.com/path?foo=bar'); + static::assertSame('/path?foo=bar', $req->getRequestTarget()); + } + + public function testGetRequestTargetWithEmptyPath(): void + { + $req = Request::get('http://example.com'); + // When path is empty, should return '/' + static::assertSame('/', $req->getRequestTarget()); + } + + public function testGetUriOrNull(): void + { + $req = new Request(Http::GET); + static::assertNull($req->getUriOrNull()); + + $req2 = Request::get('http://example.com/'); + static::assertNotNull($req2->getUriOrNull()); + } + + public function testGetUriThrowsWhenNotSet(): void + { + $this->expectException(RequestException::class); + $req = new Request(Http::GET); + $req->getUri(); + } + + public function testGetUriString(): void + { + $req = Request::get('http://example.com/path'); + static::assertSame('http://example.com/path', $req->getUriString()); + } + + public function testGetProtocolVersion(): void + { + $req = Request::get('http://example.com/'); + static::assertSame(Http::HTTP_1_1, $req->getProtocolVersion()); + + $req2 = $req->withProtocolVersion(Http::HTTP_2_0); + static::assertSame(Http::HTTP_2_0, $req2->getProtocolVersion()); + } + + public function testWithMethod(): void + { + $req = Request::get('http://example.com/')->withMethod(Http::POST); + static::assertSame(Http::POST, $req->getMethod()); + } + + public function testWithHeader(): void + { + $req = Request::get('http://example.com/')->withHeader('X-Foo', 'bar'); + static::assertTrue($req->hasHeader('X-Foo')); + static::assertSame(['bar'], $req->getHeader('X-Foo')); + static::assertSame('bar', $req->getHeaderLine('X-Foo')); + } + + public function testWithHeaderMultipleValues(): void + { + $req = Request::get('http://example.com/') + ->withHeader('X-Multi', ['a', 'b']); + static::assertSame(['a', 'b'], $req->getHeader('X-Multi')); + static::assertSame('a, b', $req->getHeaderLine('X-Multi')); + } + + public function testGetHeaderReturnsEmptyArrayForMissingHeader(): void + { + $req = Request::get('http://example.com/'); + static::assertSame([], $req->getHeader('X-Does-Not-Exist')); + static::assertSame('', $req->getHeaderLine('X-Does-Not-Exist')); + } + + public function testWithAddedHeaderAppendsValues(): void + { + $req = Request::get('http://example.com/') + ->withHeader('X-Foo', 'first') + ->withAddedHeader('X-Foo', 'second'); + $values = $req->getHeader('X-Foo'); + static::assertContains('first', $values); + static::assertContains('second', $values); + } + + public function testWithAddedHeaderInvalidNameThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + Request::get('http://example.com/')->withAddedHeader('', 'value'); + } + + public function testWithoutHeader(): void + { + $req = Request::get('http://example.com/') + ->withHeader('X-Foo', 'bar') + ->withoutHeader('X-Foo'); + static::assertFalse($req->hasHeader('X-Foo')); + } + + public function testWithHeaders(): void + { + $req = Request::get('http://example.com/') + ->withHeaders(['X-A' => 'a', 'X-B' => 'b']); + static::assertTrue($req->hasHeader('X-A')); + static::assertTrue($req->hasHeader('X-B')); + } + + public function testWithBody(): void + { + $stream = Stream::createNotNull('hello'); + $req = Request::post('http://example.com/')->withBody($stream); + static::assertInstanceOf(Request::class, $req); + } + + public function testGetBodyReturnsStream(): void + { + $req = Request::post('http://example.com/', 'payload'); + static::assertInstanceOf(\Psr\Http\Message\StreamInterface::class, $req->getBody()); + } + + public function testWithUri(): void + { + $uri = new Uri('http://other.com/path'); + $req = Request::get('http://example.com/')->withUri($uri); + static::assertSame('http://other.com/path', (string) $req->getUri()); + } + + public function testWithUriPreserveHost(): void + { + $req = Request::get('http://example.com/') + ->withHeader('Host', 'original.com'); + $uri = new Uri('http://other.com/path'); + $new = $req->withUri($uri, true); + // Host header preserved when preserveHost=true AND host already set + static::assertSame('original.com', $new->getHeaderLine('Host')); + } + + public function testWithRequestTarget(): void + { + $req = Request::get('http://example.com/old') + ->withRequestTarget('/new-path'); + static::assertSame('/new-path', $req->getRequestTarget()); + } + + public function testWithRequestTargetThrowsOnWhitespace(): void + { + $this->expectException(\InvalidArgumentException::class); + Request::get('http://example.com/')->withRequestTarget('/path with space'); + } + + public function testWithRequestTargetNoUri(): void + { + $req = new Request(Http::GET); + $new = $req->withRequestTarget('/something'); + // No URI set – request target is set, method should not throw + static::assertInstanceOf(Request::class, $new); + } + + public function testWithCookie(): void + { + $req = Request::get('http://example.com/')->withCookie('session', 'abc123'); + static::assertSame('session=abc123', $req->getHeaderLine('Cookie')); + } + + public function testWithAddedCookie(): void + { + $req = Request::get('http://example.com/') + ->withCookie('a', '1') + ->withAddedCookie('b', '2'); + $cookie = $req->getHeaderLine('Cookie'); + static::assertStringContainsString('a=1', $cookie); + } + + public function testWithUserAgent(): void + { + $req = Request::get('http://example.com/')->withUserAgent('MyClient/1.0'); + static::assertSame('MyClient/1.0', $req->getHeaderLine('User-Agent')); + } + + public function testWithCurlOption(): void + { + $req = Request::get('http://example.com/')->withCurlOption(\CURLOPT_VERBOSE, true); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithMimeType(): void + { + $req = Request::get('http://example.com/')->withMimeType(Mime::JSON); + static::assertSame(Mime::getFullMime(Mime::JSON), $req->getContentType()); + static::assertSame(Mime::getFullMime(Mime::JSON), $req->getExpectedType()); + } + + public function testWithMimeTypeNull(): void + { + $req = Request::get('http://example.com/')->withMimeType(null); + static::assertInstanceOf(Request::class, $req); + } + + public function testClientSideCertAuth(): void + { + $req = Request::get('http://example.com/') + ->clientSideCertAuth('/path/to/cert.pem', '/path/to/key.pem', 'secret', 'PEM'); + static::assertTrue($req->hasClientSideCert()); + } + + public function testHasClientSideCertFalseByDefault(): void + { + $req = Request::get('http://example.com/'); + static::assertFalse($req->hasClientSideCert()); + } + + public function testSerializePayloadMode(): void + { + $req = Request::get('http://example.com/') + ->serializePayloadMode(Request::SERIALIZE_PAYLOAD_ALWAYS); + static::assertSame(Request::SERIALIZE_PAYLOAD_ALWAYS, $req->getSerializePayloadMethod()); + + $req2 = $req->neverSerializePayload(); + static::assertSame(Request::SERIALIZE_PAYLOAD_NEVER, $req2->getSerializePayloadMethod()); + + $req3 = $req2->smartSerializePayload(); + static::assertSame(Request::SERIALIZE_PAYLOAD_SMART, $req3->getSerializePayloadMethod()); + } + + public function testRegisterPayloadSerializer(): void + { + $callback = static function ($p) { + return json_encode($p); + }; + $req = Request::get('http://example.com/') + ->registerPayloadSerializer(Mime::JSON, $callback); + static::assertInstanceOf(Request::class, $req); + } + + public function testWithSerializePayload(): void + { + $callback = static function ($p) { + return serialize($p); + }; + $req = Request::get('http://example.com/')->withSerializePayload($callback); + static::assertInstanceOf(Request::class, $req); + } + + public function testGetPayloadAndGetSerializedPayload(): void + { + $req = Request::post('http://example.com/', 'body'); + static::assertIsArray($req->getPayload()); + // Before sending, serialized_payload is null + static::assertNull($req->getSerializedPayload()); + } + + public function testGetRawHeaders(): void + { + $req = Request::get('http://example.com/'); + // Before a send, raw_headers is empty + static::assertIsString($req->getRawHeaders()); + } + + public function testHelperData(): void + { + $req = Request::get('http://example.com/') + ->addHelperData('key', 'value') + ->addHelperData('other', 42); + + static::assertSame('value', $req->getHelperData('key')); + static::assertSame(42, $req->getHelperData('other')); + static::assertNull($req->getHelperData('missing')); + static::assertSame('default', $req->getHelperData('missing', 'default')); + static::assertIsArray($req->getHelperData()); + + $req->clearHelperData(); + static::assertSame([], $req->getHelperData()); + } + + public function testHasProxyFalseByDefault(): void + { + $req = Request::get('http://example.com/'); + static::assertFalse($req->hasProxy()); + } + + public function testGetHeadersReturnsArray(): void + { + $req = Request::get('http://example.com/')->withHeader('X-Test', 'value'); + $headers = $req->getHeaders(); + static::assertIsArray($headers); + } + + public function testWithDownload(): void + { + $req = Request::get('http://example.com/') + ->withUriFromString('http://example.com/file.zip') + ->withDownload('/tmp/file.zip'); + static::assertInstanceOf(Request::class, $req); + } + + // ========================================================================= + // Stream – error paths and untested methods + // ========================================================================= + + public function testStreamWriteAndRead(): void + { + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + + $written = $stream->write('hello world'); + static::assertSame(11, $written); + + $stream->seek(0); + static::assertSame('hello world', $stream->read(11)); + } + + public function testStreamTell(): void + { + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->write('abc'); + static::assertSame(3, $stream->tell()); + } + + public function testStreamEof(): void + { + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->write('x'); + $stream->seek(0); + static::assertFalse($stream->eof()); + $stream->read(10); // read past end + static::assertTrue($stream->eof()); + } + + public function testStreamGetSize(): void + { + $stream = Stream::createNotNull('hello'); + static::assertSame(5, $stream->getSize()); + } + + public function testStreamGetSizeWithSizeOption(): void + { + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource, ['size' => 99]); + static::assertSame(99, $stream->getSize()); + } + + public function testStreamGetContentsUnserialized(): void + { + $data = ['a' => 1, 'b' => 2]; + $stream = Stream::createNotNull($data); + // Stream::create writes to the buffer but leaves cursor at end; + // seek back to start before reading. + $stream->seek(0); + $result = $stream->getContentsUnserialized(); + static::assertSame($data, $result); + } + + public function testStreamDetach(): void + { + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $detached = $stream->detach(); + static::assertIsResource($detached); + // After detach, is not seekable/readable/writable + static::assertFalse($stream->isSeekable()); + static::assertFalse($stream->isReadable()); + static::assertFalse($stream->isWritable()); + } + + public function testStreamDetachTwiceReturnsNull(): void + { + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->detach(); + static::assertNull($stream->detach()); + } + + public function testStreamGetMetadataAfterDetach(): void + { + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->detach(); + static::assertSame([], $stream->getMetadata()); + static::assertNull($stream->getMetadata('uri')); + } + + public function testStreamGetSizeAfterDetach(): void + { + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->detach(); + static::assertNull($stream->getSize()); + } + + public function testStreamEofThrowsWhenDetached(): void + { + $this->expectException(\RuntimeException::class); + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->detach(); + $stream->eof(); + } + + public function testStreamSeekThrowsWhenDetached(): void + { + $this->expectException(\RuntimeException::class); + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->detach(); + $stream->seek(0); + } + + public function testStreamTellThrowsWhenDetached(): void + { + $this->expectException(\RuntimeException::class); + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->detach(); + $stream->tell(); + } + + public function testStreamReadThrowsWhenDetached(): void + { + $this->expectException(\RuntimeException::class); + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->detach(); + $stream->read(1); + } + + public function testStreamWriteThrowsWhenDetached(): void + { + $this->expectException(\RuntimeException::class); + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->detach(); + $stream->write('x'); + } + + public function testStreamGetContentsThrowsWhenDetached(): void + { + $this->expectException(\RuntimeException::class); + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + $stream->detach(); + $stream->getContents(); + } + + public function testStreamReadLengthZero(): void + { + $stream = Stream::createNotNull('hello'); + static::assertSame('', $stream->read(0)); + } + + public function testStreamReadNegativeLengthThrows(): void + { + $this->expectException(\RuntimeException::class); + $stream = Stream::createNotNull('hello'); + $stream->read(-1); + } + + public function testStreamReadFromNonReadableThrows(): void + { + $this->expectException(\RuntimeException::class); + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource); + // Force non-readable + $stream->detach(); + fclose($resource); + // create a write-only stream + $tmpFile = \tempnam(\sys_get_temp_dir(), 'stream_test_'); + $wo = \fopen($tmpFile, 'wb'); + $stream2 = new Stream($wo); + $stream2->read(1); + // cleanup + @unlink($tmpFile); + } + + public function testStreamWriteToNonWritableThrows(): void + { + $this->expectException(\RuntimeException::class); + $tmpFile = \tempnam(\sys_get_temp_dir(), 'stream_test_'); + \file_put_contents($tmpFile, 'content'); + $ro = \fopen($tmpFile, 'rb'); + $stream = new Stream($ro); + $stream->write('data'); + @unlink($tmpFile); + } + + public function testStreamCreateWithNull(): void + { + $stream = Stream::create(null); + static::assertNotNull($stream); + static::assertSame('', (string) $stream); + } + + public function testStreamCreateWithNumeric(): void + { + $stream = Stream::create(42); + static::assertNotNull($stream); + static::assertSame('42', (string) $stream); + } + + public function testStreamCreateNotNull(): void + { + $stream = Stream::createNotNull('test'); + static::assertInstanceOf(Stream::class, $stream); + } + + public function testStreamCustomMetadata(): void + { + $resource = \fopen('php://temp', 'r+b'); + $stream = new Stream($resource, ['metadata' => ['custom_key' => 'custom_value']]); + static::assertSame('custom_value', $stream->getMetadata('custom_key')); + $all = $stream->getMetadata(); + static::assertArrayHasKey('custom_key', $all); + } + + public function testStreamToStringAfterDetach(): void + { + $resource = \fopen('php://temp', 'r+b'); + \fwrite($resource, 'hello'); + $stream = new Stream($resource); + $stream->detach(); + static::assertSame('', (string) $stream); + } + + public function testStreamIsSeekable(): void + { + $stream = Stream::createNotNull('data'); + static::assertTrue($stream->isSeekable()); + static::assertTrue($stream->isReadable()); + static::assertTrue($stream->isWritable()); + } + + public function testStreamRewind(): void + { + $stream = Stream::createNotNull('hello world'); + $stream->read(5); + $stream->rewind(); + static::assertSame(0, $stream->tell()); + } + + // ========================================================================= + // Headers – exception paths and validation + // ========================================================================= + + public function testHeadersOffsetSetThrows(): void + { + $this->expectException(ResponseHeaderException::class); + $headers = new Headers(['X-Foo' => ['bar']]); + $headers['X-New'] = 'value'; + } + + public function testHeadersOffsetUnsetThrows(): void + { + $this->expectException(ResponseHeaderException::class); + $headers = new Headers(['X-Foo' => ['bar']]); + unset($headers['X-Foo']); + } + + public function testHeadersFromString(): void + { + $raw = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nX-Foo: bar\r\n"; + $headers = Headers::fromString($raw); + static::assertSame(['application/json'], $headers->offsetGet('Content-Type')); + static::assertSame(['bar'], $headers->offsetGet('X-Foo')); + } + + public function testHeadersFromStringWithMultipleValues(): void + { + $raw = "HTTP/1.1 200 OK\r\nSet-Cookie: a=1\r\nSet-Cookie: b=2\r\n"; + $headers = Headers::fromString($raw); + static::assertIsArray($headers->offsetGet('Set-Cookie')); + } + + public function testHeadersFromStringWithNoColon(): void + { + // Lines without ':' should be skipped + $raw = "HTTP/1.1 200 OK\r\nInvalidLine\r\nX-Valid: yes\r\n"; + $headers = Headers::fromString($raw); + static::assertSame(['yes'], $headers->offsetGet('X-Valid')); + static::assertFalse($headers->offsetExists('InvalidLine')); + } + + public function testHeadersCaseInsensitive(): void + { + $headers = new Headers(['Content-Type' => ['application/json']]); + static::assertTrue($headers->offsetExists('content-type')); + static::assertTrue($headers->offsetExists('CONTENT-TYPE')); + } + + public function testHeadersCount(): void + { + $headers = new Headers(['A' => ['1'], 'B' => ['2']]); + static::assertSame(2, $headers->count()); + } + + public function testHeadersIteration(): void + { + $headers = new Headers(['X-A' => ['1'], 'X-B' => ['2']]); + $keys = []; + foreach ($headers as $k => $v) { + $keys[] = $k; + } + static::assertContains('X-A', $keys); + static::assertContains('X-B', $keys); + } + + public function testHeadersToArray(): void + { + $headers = new Headers(['Content-Type' => ['text/plain']]); + $arr = $headers->toArray(); + static::assertArrayHasKey('Content-Type', $arr); + } + + public function testHeadersForceUnset(): void + { + $headers = new Headers(['X-Remove' => ['me']]); + $headers->forceUnset('X-Remove'); + static::assertFalse($headers->offsetExists('X-Remove')); + } + + public function testHeadersInvalidNameThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + new Headers(['' => ['value']]); + } + + public function testHeadersInvalidValueThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + // Null value is not a valid header value + new Headers(['X-Foo' => [null]]); + } + + public function testHeadersEmptyArrayThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + new Headers(['X-Foo' => []]); + } + + // ========================================================================= + // Uri – static helper methods not yet covered + // ========================================================================= + + public function testUriIsAbsolute(): void + { + static::assertTrue(Uri::isAbsolute(new Uri('http://example.com/'))); + static::assertFalse(Uri::isAbsolute(new Uri('/path'))); + static::assertFalse(Uri::isAbsolute(new Uri('//example.com/path'))); + } + + public function testUriIsDefaultPort(): void + { + static::assertTrue(Uri::isDefaultPort(new Uri('http://example.com/'))); + static::assertTrue(Uri::isDefaultPort(new Uri('http://example.com:80/'))); + static::assertFalse(Uri::isDefaultPort(new Uri('http://example.com:8080/'))); + // No scheme → no default port definition, port explicitly set + static::assertFalse(Uri::isDefaultPort(new Uri('//example.com:12345/'))); + } + + public function testUriIsNetworkPathReference(): void + { + static::assertTrue(Uri::isNetworkPathReference(new Uri('//example.com/path'))); + static::assertFalse(Uri::isNetworkPathReference(new Uri('http://example.com/'))); + static::assertFalse(Uri::isNetworkPathReference(new Uri('/path'))); + } + + public function testUriIsAbsolutePathReference(): void + { + static::assertTrue(Uri::isAbsolutePathReference(new Uri('/path/to/resource'))); + static::assertFalse(Uri::isAbsolutePathReference(new Uri('http://example.com/'))); + static::assertFalse(Uri::isAbsolutePathReference(new Uri('relative/path'))); + static::assertFalse(Uri::isAbsolutePathReference(new Uri(''))); + } + + public function testUriIsRelativePathReference(): void + { + static::assertTrue(Uri::isRelativePathReference(new Uri('relative/path'))); + static::assertTrue(Uri::isRelativePathReference(new Uri(''))); + static::assertFalse(Uri::isRelativePathReference(new Uri('/absolute/path'))); + static::assertFalse(Uri::isRelativePathReference(new Uri('http://example.com/'))); + } + + public function testUriIsSameDocumentReference(): void + { + static::assertTrue(Uri::isSameDocumentReference(new Uri(''))); + static::assertTrue(Uri::isSameDocumentReference(new Uri('#fragment'))); + static::assertFalse(Uri::isSameDocumentReference(new Uri('http://example.com/'))); + + $base = new Uri('http://example.com/path?q=1'); + static::assertTrue(Uri::isSameDocumentReference(new Uri(''), $base)); + static::assertTrue(Uri::isSameDocumentReference(new Uri('http://example.com/path?q=1'), $base)); + static::assertFalse(Uri::isSameDocumentReference(new Uri('http://example.com/other'), $base)); + } + + public function testUriWithQueryValue(): void + { + $uri = new Uri('http://example.com/path?foo=1&bar=2'); + $new = Uri::withQueryValue($uri, 'foo', 'replaced'); + static::assertStringContainsString('foo=replaced', (string) $new); + static::assertStringContainsString('bar=2', (string) $new); + } + + public function testUriWithQueryValueNullRemovesValue(): void + { + $uri = new Uri('http://example.com/path?foo=1'); + $new = Uri::withQueryValue($uri, 'foo', null); + static::assertStringContainsString('foo', (string) $new); + // When value is null, key is present without '=' + static::assertStringNotContainsString('foo=', (string) $new); + } + + public function testUriWithQueryValues(): void + { + $uri = new Uri('http://example.com/path'); + $new = Uri::withQueryValues($uri, ['a' => '1', 'b' => '2']); + static::assertStringContainsString('a=1', (string) $new); + static::assertStringContainsString('b=2', (string) $new); + } + + public function testUriWithoutQueryValue(): void + { + $uri = new Uri('http://example.com/path?foo=1&bar=2'); + $new = Uri::withoutQueryValue($uri, 'foo'); + static::assertStringNotContainsString('foo', (string) $new); + static::assertStringContainsString('bar=2', (string) $new); + } + + public function testUriFromParts(): void + { + $uri = Uri::fromParts([ + 'scheme' => 'https', + 'host' => 'example.com', + 'path' => '/path', + 'query' => 'key=value', + ]); + static::assertSame('https', $uri->getScheme()); + static::assertSame('example.com', $uri->getHost()); + static::assertSame('/path', $uri->getPath()); + static::assertSame('key=value', $uri->getQuery()); + } + + public function testUriComposeComponentsFileScheme(): void + { + $result = Uri::composeComponents('file', '', '/etc/hosts', '', ''); + static::assertSame('file:///etc/hosts', $result); + } + + public function testUriWithPortRemovesDefaultPort(): void + { + $uri = new Uri('http://example.com:80/path'); + // Default port 80 for http should be stripped + static::assertNull($uri->getPort()); + } + + public function testUriWithPortNonDefault(): void + { + $uri = new Uri('http://example.com:8080/path'); + static::assertSame(8080, $uri->getPort()); + } + + public function testUriWithPortInvalidThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + $uri = new Uri('http://example.com/'); + $uri->withPort(99999); + } + + public function testUriWithScheme(): void + { + $uri = new Uri('http://example.com/'); + $new = $uri->withScheme('https'); + static::assertSame('https', $new->getScheme()); + // Same scheme returns same instance + static::assertSame($new, $new->withScheme('https')); + } + + public function testUriWithFragment(): void + { + $uri = new Uri('http://example.com/path'); + $new = $uri->withFragment('section1'); + static::assertSame('section1', $new->getFragment()); + static::assertStringContainsString('#section1', (string) $new); + // Same fragment returns same instance + static::assertSame($new, $new->withFragment('section1')); + } + + public function testUriWithQuery(): void + { + $uri = new Uri('http://example.com/path'); + $new = $uri->withQuery('foo=bar'); + static::assertSame('foo=bar', $new->getQuery()); + // Same query returns same instance + static::assertSame($new, $new->withQuery('foo=bar')); + } + + public function testUriWithUserInfo(): void + { + $uri = new Uri('http://example.com/'); + $new = $uri->withUserInfo('user', 'pass'); + static::assertSame('user:pass', $new->getUserInfo()); + static::assertStringContainsString('user:pass@', (string) $new); + // Same user info returns same instance + static::assertSame($new, $new->withUserInfo('user', 'pass')); + } + + public function testUriGetAuthorityNoHost(): void + { + $uri = new Uri('/path'); + static::assertSame('', $uri->getAuthority()); + } + + public function testUriInvalidThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + new Uri('http:///::'); + } + + // ========================================================================= + // CsvMimeHandler – serialize + // ========================================================================= + + public function testCsvMimeHandlerSerialize(): void + { + $handler = new CsvMimeHandler(); + $data = [ + ['name' => 'Alice', 'age' => '30'], + ['name' => 'Bob', 'age' => '25'], + ]; + $csv = $handler->serialize($data); + static::assertIsString($csv); + static::assertStringContainsString('Alice', $csv); + static::assertStringContainsString('Bob', $csv); + static::assertStringContainsString('name', $csv); + static::assertStringContainsString('age', $csv); + } + + public function testCsvMimeHandlerParseEmpty(): void + { + $handler = new CsvMimeHandler(); + static::assertNull($handler->parse('')); + } + + public function testCsvMimeHandlerParse(): void + { + $handler = new CsvMimeHandler(); + $result = $handler->parse("a,b,c\n1,2,3\n"); + static::assertIsArray($result); + static::assertCount(2, $result); + } + + // ========================================================================= + // DefaultMimeHandler – serialize with Serializable + // ========================================================================= + + public function testDefaultMimeHandlerSerializeArray(): void + { + $handler = new DefaultMimeHandler(); + $data = ['key' => 'value']; + $result = $handler->serialize($data); + static::assertIsString($result); + static::assertSame(\serialize($data), $result); + } + + public function testDefaultMimeHandlerSerializeScalar(): void + { + $handler = new DefaultMimeHandler(); + static::assertSame('hello', $handler->serialize('hello')); + static::assertSame(42, $handler->serialize(42)); + } + + public function testDefaultMimeHandlerParseReturnsSame(): void + { + $handler = new DefaultMimeHandler(); + static::assertSame('body content', $handler->parse('body content')); + } + + // ========================================================================= + // XmlMimeHandler – serialize_clean and serialize_node + // ========================================================================= + + public function testXmlMimeHandlerSerializeClean(): void + { + $handler = new XmlMimeHandler(); + $xml = $handler->serialize_clean(['root' => ['child' => 'value']]); + static::assertStringContainsString('', $xml); + static::assertStringContainsString('', $xml); + static::assertStringContainsString('value', $xml); + } + + public function testXmlMimeHandlerSerializeNode(): void + { + $writer = new \XMLWriter(); + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $handler = new XmlMimeHandler(); + $writer->startElement('root'); + $handler->serialize_node($writer, 'text node'); + $writer->endElement(); + $xml = $writer->outputMemory(true); + static::assertStringContainsString('text node', $xml); + } + + public function testXmlMimeHandlerSerializeNodeArray(): void + { + $writer = new \XMLWriter(); + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $handler = new XmlMimeHandler(); + $writer->startElement('wrapper'); + $handler->serialize_node($writer, ['item' => 'value1']); + $writer->endElement(); + $xml = $writer->outputMemory(true); + static::assertStringContainsString('', $xml); + static::assertStringContainsString('value1', $xml); + } + + public function testXmlMimeHandlerSerializePayload(): void + { + $handler = new XmlMimeHandler(); + $xml = $handler->serialize(['key' => 'val']); + static::assertStringContainsString('', (string) $xml); + } + + // ========================================================================= + // HtmlMimeHandler – serialize + // ========================================================================= + + public function testHtmlMimeHandlerSerialize(): void + { + $handler = new HtmlMimeHandler(); + static::assertSame('

Hello

', $handler->serialize('

Hello

')); + } + + public function testHtmlMimeHandlerParseEmpty(): void + { + $handler = new HtmlMimeHandler(); + static::assertNull($handler->parse('')); + } + + // ========================================================================= + // UploadedFile – various states + // ========================================================================= + + public function testUploadedFileWithStream(): void + { + $stream = Stream::createNotNull('file contents'); + $file = new UploadedFile($stream, 13, \UPLOAD_ERR_OK, 'test.txt', 'text/plain'); + static::assertSame(13, $file->getSize()); + static::assertSame(\UPLOAD_ERR_OK, $file->getError()); + static::assertSame('test.txt', $file->getClientFilename()); + static::assertSame('text/plain', $file->getClientMediaType()); + static::assertSame($stream, $file->getStream()); + } + + public function testUploadedFileWithResource(): void + { + $resource = \fopen('php://temp', 'r+b'); + \fwrite($resource, 'data'); + \rewind($resource); + $file = new UploadedFile($resource, 4, \UPLOAD_ERR_OK); + static::assertInstanceOf(\Psr\Http\Message\StreamInterface::class, $file->getStream()); + } + + public function testUploadedFileWithFilePath(): void + { + $tmpFile = \tempnam(\sys_get_temp_dir(), 'upf_'); + \file_put_contents($tmpFile, 'content'); + $file = new UploadedFile($tmpFile, 7, \UPLOAD_ERR_OK, 'upload.txt'); + $stream = $file->getStream(); + static::assertInstanceOf(\Psr\Http\Message\StreamInterface::class, $stream); + @unlink($tmpFile); + } + + public function testUploadedFileGetStreamThrowsOnError(): void + { + $this->expectException(\RuntimeException::class); + $file = new UploadedFile('', 0, \UPLOAD_ERR_NO_FILE); + $file->getStream(); + } + + public function testUploadedFileMoveToWithStream(): void + { + $stream = Stream::createNotNull('move me'); + $file = new UploadedFile($stream, 7, \UPLOAD_ERR_OK); + $dest = \tempnam(\sys_get_temp_dir(), 'moved_'); + $file->moveTo($dest); + static::assertSame('move me', \file_get_contents($dest)); + @unlink($dest); + } + + public function testUploadedFileMoveToThrowsAfterMove(): void + { + $this->expectException(\RuntimeException::class); + $stream = Stream::createNotNull('data'); + $file = new UploadedFile($stream, 4, \UPLOAD_ERR_OK); + $dest = \tempnam(\sys_get_temp_dir(), 'moved_'); + $file->moveTo($dest); + // Second moveTo should throw + $file->moveTo($dest); + @unlink($dest); + } + + public function testUploadedFileMoveToInvalidPathThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + $stream = Stream::createNotNull('data'); + $file = new UploadedFile($stream, 4, \UPLOAD_ERR_OK); + $file->moveTo(''); + } + + public function testUploadedFileInvalidErrorStatusThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + new UploadedFile('file.txt', 10, 999); + } + + public function testUploadedFileInvalidSizeThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + new UploadedFile('file.txt', 'not-an-int', \UPLOAD_ERR_OK); + } + + public function testUploadedFileInvalidClientFilenameThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + new UploadedFile('file.txt', 10, \UPLOAD_ERR_OK, 12345); + } + + public function testUploadedFileInvalidClientMediaTypeThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + new UploadedFile('file.txt', 10, \UPLOAD_ERR_OK, null, 12345); + } + + public function testUploadedFileInvalidStreamOrFileThrows(): void + { + $this->expectException(\InvalidArgumentException::class); + new UploadedFile(12345, 10, \UPLOAD_ERR_OK); + } + + // ========================================================================= + // Exception classes + // ========================================================================= + + public function testNetworkErrorExceptionGetRequest(): void + { + $req = Request::get('http://example.com/'); + $ex = new NetworkErrorException('error', 0, null, null, $req); + static::assertSame($req, $ex->getRequest()); + } + + public function testNetworkErrorExceptionGetRequestDefaultsToNew(): void + { + $ex = new NetworkErrorException('error'); + static::assertInstanceOf(Request::class, $ex->getRequest()); + } + + public function testNetworkErrorExceptionSetters(): void + { + $ex = new NetworkErrorException('err'); + $ex->setCurlErrorNumber(28); + $ex->setCurlErrorString('Timeout'); + static::assertSame(28, $ex->getCurlErrorNumber()); + static::assertSame('Timeout', $ex->getCurlErrorString()); + } + + public function testNetworkErrorExceptionWasTimeout(): void + { + $ex = new NetworkErrorException('timeout', \CURLE_OPERATION_TIMEOUTED); + static::assertTrue($ex->wasTimeout()); + + $ex2 = new NetworkErrorException('other', 0); + static::assertFalse($ex2->wasTimeout()); + } + + public function testNetworkErrorExceptionGetCurlObject(): void + { + $ex = new NetworkErrorException('err'); + static::assertNull($ex->getCurlObject()); + } + + public function testClientErrorException(): void + { + $ex = new ClientErrorException('client error'); + static::assertSame('client error', $ex->getMessage()); + } + + public function testRequestExceptionGetRequest(): void + { + $req = Request::get('http://example.com/'); + $ex = new RequestException($req, 'something went wrong'); + static::assertSame($req, $ex->getRequest()); + } + + // ========================================================================= + // Response – untested methods + // ========================================================================= + + public function testResponseGetStatusCode(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('body', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + static::assertSame(200, $response->getStatusCode()); + } + + public function testResponseGetReasonPhrase(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 404 Not Found\r\n\r\n", $req, []); + static::assertSame('Not Found', $response->getReasonPhrase()); + } + + public function testResponseWithStatus(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + $new = $response->withStatus(201, 'Created'); + static::assertSame(201, $new->getStatusCode()); + static::assertSame('Created', $new->getReasonPhrase()); + } + + public function testResponseGetProtocolVersion(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, ['protocol_version' => '1.1']); + static::assertSame('1.1', $response->getProtocolVersion()); + } + + public function testResponseWithProtocolVersion(): void + { + $req = Request::get('http://example.com/'); + $response = new Response('', "HTTP/1.1 200 OK\r\n\r\n", $req, []); + $new = $response->withProtocolVersion('2.0'); + static::assertSame('2.0', $new->getProtocolVersion()); + } +}