<?php
use PHPUnit\Framework\TestCase;
use SHServ\Tools\DeviceAPI\Base;
class DeviceAPIBaseRetryTest extends TestCase {
private function makeSuccessResponse(array $data, int $httpCode = 200): array {
$body = json_encode($data);
$header = "HTTP/1.1 {$httpCode} OK\r\nContent-Type: application/json\r\n\r\n";
return [
'body' => $header . $body,
'error' => null,
'http_code' => $httpCode,
'header_size' => strlen($header),
];
}
private function makeFailureResponse(string $error = 'Connection timed out'): array {
return [
'body' => false,
'error' => $error,
'http_code' => 0,
'header_size' => 0,
];
}
public function test_success_on_first_attempt(): void {
$mock = new MockableDeviceAPI('192.168.1.10', 'test_token');
$mock -> responses = [
$this -> makeSuccessResponse(['status' => 'ok', 'device_type' => 'relay']),
];
$result = $mock -> get_about();
$this -> assertSame(200, $result['http_code']);
$this -> assertSame('ok', $result['data']['status']);
$this -> assertSame(1, $mock -> callIndex);
}
public function test_success_after_two_failures(): void {
$mock = new MockableDeviceAPI('192.168.1.10', 'test_token');
$mock -> responses = [
$this -> makeFailureResponse(),
$this -> makeFailureResponse(),
$this -> makeSuccessResponse(['status' => 'ok']),
];
$result = $mock -> get_about();
$this -> assertSame(200, $result['http_code']);
$this -> assertSame('ok', $result['data']['status']);
$this -> assertSame(3, $mock -> callIndex);
}
public function test_failure_after_max_retries(): void {
$mock = new MockableDeviceAPI('192.168.1.10', 'test_token');
$mock -> responses = [
$this -> makeFailureResponse('Timeout 1'),
$this -> makeFailureResponse('Timeout 2'),
$this -> makeFailureResponse('Timeout 3'),
];
$result = $mock -> get_about();
$this -> assertSame(0, $result['http_code']);
$this -> assertNotNull($result['error']);
$this -> assertSame(3, $mock -> callIndex);
}
public function test_invalid_json_response_returns_error(): void {
$mock = new MockableDeviceAPI('192.168.1.10', 'test_token');
$header = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n";
$mock -> responses = [
[
'body' => $header . 'not json at all',
'error' => null,
'http_code' => 200,
'header_size' => strlen($header),
],
];
$result = $mock -> get_about();
$this -> assertSame(200, $result['http_code']);
$this -> assertNull($result['data']);
$this -> assertSame('Invalid JSON response from device', $result['error']);
}
public function test_get_status_returns_empty_on_non_200(): void {
$mock = new MockableDeviceAPI('192.168.1.10', 'test_token');
$mock -> responses = [
$this -> makeSuccessResponse(['status' => 'ok'], 500),
];
$result = $mock -> get_status();
$this -> assertSame([], $result);
}
public function test_post_action_requires_token(): void {
$client = new Base('192.168.1.10');
$this -> expectException(\LogicException::class);
$client -> post_action('toggle', []);
}
public function test_post_action_succeeds_with_token(): void {
$mock = new MockableDeviceAPI('192.168.1.10', 'test_token');
$mock -> responses = [
$this -> makeSuccessResponse(['status' => 'ok', 'message' => 'Done']),
];
$result = $mock -> post_action('toggle', ['channel' => 1]);
$this -> assertSame(200, $result['http_code']);
$this -> assertSame('ok', $result['data']['status']);
}
}
class MockableDeviceAPI extends Base {
public array $responses = [];
public int $callIndex = 0;
protected function executeCurl($ch): array {
$response = $this -> responses[$this -> callIndex] ?? null;
$this -> callIndex++;
if ($response === null) {
return [false, 'Connection timed out'];
}
return [$response['body'], $response['error'] ?? null];
}
protected function getCurlInfo($ch, int $option) {
$response = $this -> responses[max(0, $this -> callIndex - 1)] ?? [];
if ($option === CURLINFO_HTTP_CODE) {
return $response['http_code'] ?? 0;
}
if ($option === CURLINFO_HEADER_SIZE) {
return $response['header_size'] ?? 0;
}
return 0;
}
}