Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.02% covered (success)
93.02%
40 / 43
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
MockUdpClient
93.02% covered (success)
93.02%
40 / 43
75.00% covered (warning)
75.00%
3 / 4
25.21
0.00% covered (danger)
0.00%
0 / 1
 loadFixture
89.66% covered (warning)
89.66%
26 / 29
0.00% covered (danger)
0.00%
0 / 1
14.22
 query
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
9
 reset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCaptureCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2
3/**
4 * Clansuite Server Query
5 *
6 * SPDX-FileCopyrightText: 2003-2025 Jens A. Koch
7 * SPDX-License-Identifier: MIT
8 *
9 * For the full copyright and license information, please view
10 * the LICENSE file that was distributed with this source code.
11 */
12
13namespace Clansuite\ServerQuery\Util;
14
15use function base64_decode;
16use function count;
17use function file_exists;
18use function file_get_contents;
19use function is_array;
20use function is_string;
21use function json_decode;
22use function microtime;
23use Exception;
24use Override;
25
26/**
27 * Mock UDP Client for replay-based testing using captured fixtures.
28 */
29final class MockUdpClient extends UdpClient
30{
31    /**
32     * @var array<array<string, mixed>>
33     */
34    private array $captures    = [];
35    private int $responseIndex = 0;
36
37    /**
38     * Load responses from a JSON fixture file.
39     */
40    public function loadFixture(string $jsonFile): bool
41    {
42        if (!file_exists($jsonFile)) {
43            return false;
44        }
45
46        $c = file_get_contents($jsonFile);
47
48        if ($c === false) {
49            throw new Exception('Could not read fixture file: ' . $jsonFile);
50        }
51
52        $metadata = json_decode($c, true);
53
54        if (!is_array($metadata) || !isset($metadata['captures']) || !is_array($metadata['captures'])) {
55            return false;
56        }
57
58        $this->captures = [];
59
60        foreach ($metadata['captures'] as $capture) {
61            if (!is_array($capture)) {
62                continue;
63            }
64
65            $sent = null;
66
67            if (isset($capture['sent']) && is_string($capture['sent'])) {
68                $sent = base64_decode($capture['sent'], true);
69
70                if ($sent === false) {
71                    $sent = null;
72                }
73            }
74
75            $received = null;
76
77            if (isset($capture['received']) && is_string($capture['received'])) {
78                $received = base64_decode($capture['received'], true);
79
80                if ($received === false) {
81                    $received = null;
82                }
83            }
84
85            $this->captures[] = [
86                'sent'      => $sent,
87                'received'  => $received,
88                'timestamp' => $capture['timestamp'] ?? microtime(true),
89            ];
90        }
91
92        $this->responseIndex = 0;
93
94        return $this->captures !== [];
95    }
96
97    /**
98     * Mock query method that returns pre-recorded responses based on request matching.
99     */
100    #[Override]
101    public function query(string $address, int $port, string $packet): ?string
102    {
103        // If we have captures with sent packets, try to match the request
104        if ($this->responseIndex < count($this->captures)) {
105            $capture = $this->captures[$this->responseIndex] ?? [];
106
107            // If the capture has a sent packet, check if it matches (or is empty)
108            $sentExists = isset($capture['sent']) && is_string($capture['sent']);
109
110            if ($sentExists && ($capture['sent'] === '' || $capture['sent'] === $packet)) {
111                $response = $capture['received'] ?? null;
112                $this->responseIndex++;
113
114                return is_string($response) ? $response : null;
115            }
116        }
117
118        // Fallback: return responses in sequence regardless of request matching
119        if ($this->responseIndex < count($this->captures)) {
120            $response = $this->captures[$this->responseIndex]['received'] ?? null;
121            $this->responseIndex++;
122
123            return is_string($response) ? $response : null;
124        }
125
126        return null; // No more responses
127    }
128
129    /**
130     * Reset the mock for reuse.
131     */
132    public function reset(): void
133    {
134        $this->responseIndex = 0;
135    }
136
137    /**
138     * Get the number of available captures.
139     */
140    public function getCaptureCount(): int
141    {
142        return count($this->captures);
143    }
144}