Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
36 / 36 |
|
100.00% |
11 / 11 |
CRAP | |
100.00% |
1 / 1 |
| Mock | |
100.00% |
36 / 36 |
|
100.00% |
11 / 11 |
20 | |
100.00% |
1 / 1 |
| queue | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| when | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| setVerifyPeer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| allowSelfSigned | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getRequests | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getLastRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
| prepare | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
| send | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
| resolveResult | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
4 | |||
| reset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| disconnect | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | declare(strict_types=1); |
| 3 | /** |
| 4 | * Pop PHP Framework (https://www.popphp.org/) |
| 5 | * |
| 6 | * @link https://github.com/popphp/popphp-framework |
| 7 | * @author Nick Sagona, III <nick@popphp.org> |
| 8 | * @copyright Copyright (c) 2009-2026 Nick Sagona, III |
| 9 | * @license https://www.popphp.org/license New BSD License |
| 10 | */ |
| 11 | |
| 12 | /** |
| 13 | * @namespace |
| 14 | */ |
| 15 | namespace Pop\Http\Client\Handler; |
| 16 | |
| 17 | use Pop\Http\Auth; |
| 18 | use Pop\Http\Client\Request; |
| 19 | use Pop\Http\Client\Response; |
| 20 | |
| 21 | /** |
| 22 | * HTTP client mock handler class |
| 23 | * |
| 24 | * @category Pop |
| 25 | * @package Pop\Http |
| 26 | * @author Nick Sagona, III <nick@popphp.org> |
| 27 | * @copyright Copyright (c) 2009-2026 Nick Sagona, III |
| 28 | * @license https://www.popphp.org/license New BSD License |
| 29 | * @version 6.0.0 |
| 30 | */ |
| 31 | class Mock extends AbstractHandler |
| 32 | { |
| 33 | |
| 34 | /** |
| 35 | * Queued responses/throwables, FIFO |
| 36 | * @var array |
| 37 | */ |
| 38 | protected array $queue = []; |
| 39 | |
| 40 | /** |
| 41 | * Registered request matchers, checked in registration order before |
| 42 | * falling back to the queue |
| 43 | * @var array |
| 44 | */ |
| 45 | protected array $matchers = []; |
| 46 | |
| 47 | /** |
| 48 | * Every request actually dispatched through this handler, in order |
| 49 | * @var array |
| 50 | */ |
| 51 | protected array $history = []; |
| 52 | |
| 53 | /** |
| 54 | * Queue a response or throwable to be returned/thrown by the next send() |
| 55 | * that doesn't match a registered matcher |
| 56 | * |
| 57 | * @param Response|\Throwable $result |
| 58 | * @return Mock |
| 59 | */ |
| 60 | public function queue(Response|\Throwable $result): Mock |
| 61 | { |
| 62 | $this->queue[] = $result; |
| 63 | return $this; |
| 64 | } |
| 65 | |
| 66 | /** |
| 67 | * Register a response or throwable to return/throw for any request the |
| 68 | * given matcher accepts. Checked before the FIFO queue, in registration |
| 69 | * order - first match wins. |
| 70 | * |
| 71 | * @param callable $matcher callable(Request $request): bool |
| 72 | * @param Response|\Throwable $result |
| 73 | * @return Mock |
| 74 | */ |
| 75 | public function when(callable $matcher, Response|\Throwable $result): Mock |
| 76 | { |
| 77 | $this->matchers[] = ['matcher' => $matcher, 'result' => $result]; |
| 78 | return $this; |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * Set verify peer - no-op stub for interface parity with Curl/Stream |
| 83 | * |
| 84 | * SSL settings are meaningless for a transport that sends no bytes. This exists |
| 85 | * so Client::prepare() can duck-type call it on any non-CurlMulti handler |
| 86 | * (including Mock) without a fatal 'call to undefined method' error. |
| 87 | * |
| 88 | * @param bool $verify |
| 89 | * @return Mock |
| 90 | */ |
| 91 | public function setVerifyPeer(bool $verify = true): Mock |
| 92 | { |
| 93 | return $this; |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Allow self-signed certs - no-op stub for interface parity with Curl/Stream |
| 98 | * |
| 99 | * SSL settings are meaningless for a transport that sends no bytes. This exists |
| 100 | * so Client::prepare() can duck-type call it on any non-CurlMulti handler |
| 101 | * (including Mock) without a fatal 'call to undefined method' error. |
| 102 | * |
| 103 | * @param bool $allow |
| 104 | * @return Mock |
| 105 | */ |
| 106 | public function allowSelfSigned(bool $allow = true): Mock |
| 107 | { |
| 108 | return $this; |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Get every request actually dispatched through this handler, in order |
| 113 | * |
| 114 | * @return array |
| 115 | */ |
| 116 | public function getRequests(): array |
| 117 | { |
| 118 | return $this->history; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Get the most recently dispatched request, if any |
| 123 | * |
| 124 | * @return ?Request |
| 125 | */ |
| 126 | public function getLastRequest(): ?Request |
| 127 | { |
| 128 | return (!empty($this->history)) ? $this->history[array_key_last($this->history)] : null; |
| 129 | } |
| 130 | |
| 131 | /** |
| 132 | * Method to prepare the handler |
| 133 | * |
| 134 | * Deliberately does not call collectRequestHeaders()/resolveRequestBody() - |
| 135 | * those build wire-format output for a real transport, and Mock never sends |
| 136 | * actual bytes. It does add the auth header and prepare request data, same as |
| 137 | * Curl/Stream, so a recorded request in history/matching is semantically complete. |
| 138 | * |
| 139 | * @param Request $request |
| 140 | * @param ?Auth $auth |
| 141 | * @return Mock |
| 142 | */ |
| 143 | public function prepare(Request $request, ?Auth $auth = null): Mock |
| 144 | { |
| 145 | $this->request = $request; |
| 146 | |
| 147 | if ($auth !== null) { |
| 148 | $request->addHeader($auth->createAuthHeader()); |
| 149 | } |
| 150 | |
| 151 | if (($request->hasData()) && (!$request->getData()->isPrepared())) { |
| 152 | $request->prepareData(); |
| 153 | } |
| 154 | |
| 155 | $this->uri = $request->getUriAsString(); |
| 156 | |
| 157 | return $this; |
| 158 | } |
| 159 | |
| 160 | /** |
| 161 | * Method to send the request |
| 162 | * |
| 163 | * @throws Exception |
| 164 | * @return Response |
| 165 | */ |
| 166 | public function send(): Response |
| 167 | { |
| 168 | if ($this->request !== null) { |
| 169 | $this->history[] = clone $this->request; |
| 170 | } |
| 171 | |
| 172 | $result = $this->resolveResult(); |
| 173 | |
| 174 | if ($result instanceof \Throwable) { |
| 175 | throw $result; |
| 176 | } |
| 177 | |
| 178 | return $result; |
| 179 | } |
| 180 | |
| 181 | /** |
| 182 | * Resolve the matched/queued result for the current request - registered |
| 183 | * matchers first (registration order, first match wins), then the FIFO |
| 184 | * queue, then throw if nothing resolves |
| 185 | * |
| 186 | * @throws Exception |
| 187 | * @return Response|\Throwable |
| 188 | */ |
| 189 | protected function resolveResult(): Response|\Throwable |
| 190 | { |
| 191 | foreach ($this->matchers as $entry) { |
| 192 | if (($entry['matcher'])($this->request)) { |
| 193 | return $entry['result']; |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | if (!empty($this->queue)) { |
| 198 | return array_shift($this->queue); |
| 199 | } |
| 200 | |
| 201 | throw new Exception( |
| 202 | 'Error: No matching handler or queued response for ' . $this->request?->getMethod() . ' ' . |
| 203 | $this->request?->getUriAsString() . '.', |
| 204 | 0, null, 0, $this->request |
| 205 | ); |
| 206 | } |
| 207 | |
| 208 | /** |
| 209 | * Method to reset the handler |
| 210 | * |
| 211 | * Deliberately does not clear the queue or history - those are the test's |
| 212 | * own configuration, not per-request wire-building state. Client::prepare() |
| 213 | * calls reset() on every reused handler between sequential send() calls on |
| 214 | * the same Client; clearing test-configured state here would silently break |
| 215 | * "queue N responses for N sequential calls", the most common use of this |
| 216 | * handler. |
| 217 | * |
| 218 | * @return Mock |
| 219 | */ |
| 220 | public function reset(): Mock |
| 221 | { |
| 222 | return $this; |
| 223 | } |
| 224 | |
| 225 | /** |
| 226 | * Close the handler connection |
| 227 | * |
| 228 | * Full teardown, unlike reset() - clears the queue and history. |
| 229 | * |
| 230 | * @return void |
| 231 | */ |
| 232 | public function disconnect(): void |
| 233 | { |
| 234 | $this->queue = []; |
| 235 | $this->matchers = []; |
| 236 | $this->history = []; |
| 237 | $this->request = null; |
| 238 | } |
| 239 | |
| 240 | } |