Newer
Older
smart-home-server / server / tests / DeviceAPIBaseRetryTest.php
<?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;
	}
}