Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
93.02% |
40 / 43 |
|
75.00% |
3 / 4 |
CRAP | |
0.00% |
0 / 1 |
| MockUdpClient | |
93.02% |
40 / 43 |
|
75.00% |
3 / 4 |
25.21 | |
0.00% |
0 / 1 |
| loadFixture | |
89.66% |
26 / 29 |
|
0.00% |
0 / 1 |
14.22 | |||
| query | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
9 | |||
| reset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getCaptureCount | |
100.00% |
1 / 1 |
|
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 | |
| 13 | namespace Clansuite\ServerQuery\Util; |
| 14 | |
| 15 | use function base64_decode; |
| 16 | use function count; |
| 17 | use function file_exists; |
| 18 | use function file_get_contents; |
| 19 | use function is_array; |
| 20 | use function is_string; |
| 21 | use function json_decode; |
| 22 | use function microtime; |
| 23 | use Exception; |
| 24 | use Override; |
| 25 | |
| 26 | /** |
| 27 | * Mock UDP Client for replay-based testing using captured fixtures. |
| 28 | */ |
| 29 | final 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 | } |