Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
Mock
100.00% covered (success)
100.00%
36 / 36
100.00% covered (success)
100.00%
11 / 11
20
100.00% covered (success)
100.00%
1 / 1
 queue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 when
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setVerifyPeer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allowSelfSigned
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequests
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLastRequest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 prepare
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 send
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 resolveResult
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 reset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 disconnect
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(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 */
15namespace Pop\Http\Client\Handler;
16
17use Pop\Http\Auth;
18use Pop\Http\Client\Request;
19use 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 */
31class 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}