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());
+ }
+}