1<?php
2namespace GuzzleHttp\Tests\Ring\Client;
3
4use GuzzleHttp\Ring\Client\StreamHandler;
5use GuzzleHttp\Ring\Core;
6
7/**
8 * Class uses to control the test webserver.
9 *
10 * Queued responses will be served to requests using a FIFO order.  All requests
11 * received by the server are stored on the node.js server and can be retrieved
12 * by calling {@see Server::received()}.
13 *
14 * Mock responses that don't require data to be transmitted over HTTP a great
15 * for testing.  Mock response, however, cannot test the actual sending of an
16 * HTTP request using cURL.  This test server allows the simulation of any
17 * number of HTTP request response transactions to test the actual sending of
18 * requests over the wire without having to leave an internal network.
19 */
20class Server
21{
22    public static $started;
23    public static $url = 'http://127.0.0.1:8125/';
24    public static $host = '127.0.0.1:8125';
25    public static $port = 8125;
26
27    /**
28     * Flush the received requests from the server
29     * @throws \RuntimeException
30     */
31    public static function flush()
32    {
33        self::send('DELETE', '/guzzle-server/requests');
34    }
35
36    /**
37     * Queue an array of responses or a single response on the server.
38     *
39     * Any currently queued responses will be overwritten. Subsequent requests
40     * on the server will return queued responses in FIFO order.
41     *
42     * @param array $responses An array of responses. The shape of a response
43     *                         is the shape described in the RingPHP spec.
44     * @throws \Exception
45     */
46    public static function enqueue(array $responses)
47    {
48        $data = [];
49
50        foreach ($responses as $response) {
51            if (!is_array($response)) {
52                throw new \Exception('Each response must be an array');
53            }
54
55            if (isset($response['body'])) {
56                $response['body'] = base64_encode($response['body']);
57            }
58
59            $response += ['headers' => [], 'reason' => '', 'body' => ''];
60            $data[] = $response;
61        }
62
63        self::send('PUT', '/guzzle-server/responses', json_encode($data));
64    }
65
66    /**
67     * Get all of the received requests as a RingPHP request structure.
68     *
69     * @return array
70     * @throws \RuntimeException
71     */
72    public static function received()
73    {
74        if (!self::$started) {
75            return [];
76        }
77
78        $response = self::send('GET', '/guzzle-server/requests');
79        $body = Core::body($response);
80        $result = json_decode($body, true);
81        if ($result === false) {
82            throw new \RuntimeException('Error decoding response: '
83                . json_last_error());
84        }
85
86        foreach ($result as &$res) {
87            if (isset($res['uri'])) {
88                $res['resource'] = $res['uri'];
89            }
90            if (isset($res['query_string'])) {
91                $res['resource'] .= '?' . $res['query_string'];
92            }
93            if (!isset($res['resource'])) {
94                $res['resource'] = '';
95            }
96            // Ensure that headers are all arrays
97            if (isset($res['headers'])) {
98                foreach ($res['headers'] as &$h) {
99                    $h = (array) $h;
100                }
101                unset($h);
102            }
103        }
104
105        unset($res);
106        return $result;
107    }
108
109    /**
110     * Stop running the node.js server
111     */
112    public static function stop()
113    {
114        if (self::$started) {
115            self::send('DELETE', '/guzzle-server');
116        }
117
118        self::$started = false;
119    }
120
121    public static function wait($maxTries = 20)
122    {
123        $tries = 0;
124        while (!self::isListening() && ++$tries < $maxTries) {
125            usleep(100000);
126        }
127
128        if (!self::isListening()) {
129            throw new \RuntimeException('Unable to contact node.js server');
130        }
131    }
132
133    public static function start()
134    {
135        if (self::$started) {
136            return;
137        }
138
139        try {
140            self::wait();
141        } catch (\Exception $e) {
142            exec('node ' . __DIR__ . \DIRECTORY_SEPARATOR . 'server.js '
143                . self::$port . ' >> /tmp/server.log 2>&1 &');
144            self::wait();
145        }
146
147        self::$started = true;
148    }
149
150    private static function isListening()
151    {
152        $response = self::send('GET', '/guzzle-server/perf', null, [
153            'connect_timeout' => 1,
154            'timeout'         => 1
155        ]);
156
157        return !isset($response['error']);
158    }
159
160    private static function send(
161        $method,
162        $path,
163        $body = null,
164        array $client = []
165    ) {
166        $handler = new StreamHandler();
167
168        $request = [
169            'http_method'  => $method,
170            'uri'          => $path,
171            'request_port' => 8125,
172            'headers'      => ['host' => ['127.0.0.1:8125']],
173            'body'         => $body,
174            'client'       => $client,
175        ];
176
177        if ($body) {
178            $request['headers']['content-length'] = [strlen($body)];
179        }
180
181        return $handler($request);
182    }
183}
184