300 lines
10 KiB
PHP
300 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use Tests\Support\Api\ApiServer;
|
|
|
|
usesApiSuite();
|
|
|
|
it('rejects scanner LPR requests without an image before contacting Plate Recognizer', function (): void {
|
|
api_test_covers('POST /modules/scanner/lpr', 'failure');
|
|
|
|
$response = api_client()->post('/modules/scanner/lpr', []);
|
|
|
|
$response->assertStatus(400)->assertSuccess(false)->assertMessage('Image is required.');
|
|
});
|
|
|
|
it('accepts multipart scanner images and forwards them to Plate Recognizer as a temp-file upload', function (): void {
|
|
api_test_covers('POST /modules/scanner/lpr', 'happy');
|
|
|
|
if (trim((string)getenv('API_TEST_BASE_URL')) !== '') {
|
|
$this->markTestSkipped('This scanner LPR test requires the self-started API server so PLATE_RECOGNIZER_API_URL can be isolated.');
|
|
}
|
|
|
|
$fakePlateRecognizer = ScannerLprFakePlateRecognizerServer::start();
|
|
$previousPlateRecognizerUrl = scanner_lpr_api_get_env('PLATE_RECOGNIZER_API_URL');
|
|
scanner_lpr_api_set_env('PLATE_RECOGNIZER_API_URL', $fakePlateRecognizer->url());
|
|
api_test_runtime()->restartServer();
|
|
|
|
$imagePath = tempnam(sys_get_temp_dir(), 'scanner-lpr-api-');
|
|
expect($imagePath)->not->toBeFalse();
|
|
file_put_contents($imagePath, 'jpeg-camera-bytes');
|
|
|
|
try {
|
|
api_fixtures()->setModuleConfig('licenseplaterecognizer', 'enabled', 'true', 'bool');
|
|
api_fixtures()->setModuleConfig('licenseplaterecognizer', 'api_key', 'scanner-api-test-key', 'string');
|
|
api_test_runtime()->redis()?->del(['licenseplaterecognizer:runtime_config:v1']);
|
|
|
|
$response = api_client()->postMultipart('/modules/scanner/lpr', [
|
|
'client_capture_ms' => '12.345',
|
|
'client_frame_width' => '300',
|
|
'client_frame_height' => '225',
|
|
'client_frame_bytes' => (string)strlen('jpeg-camera-bytes'),
|
|
], [
|
|
'image' => [
|
|
'path' => $imagePath,
|
|
'mime' => 'image/jpeg',
|
|
'name' => 'camera-frame.jpg',
|
|
],
|
|
]);
|
|
|
|
$response->assertStatus(200)->assertSuccess();
|
|
expect($response->data())->toMatchArray([
|
|
'success' => true,
|
|
'license_plate_number' => 'AB12345',
|
|
]);
|
|
expect($response->headers['server-timing'] ?? '')
|
|
->toContain('lpr_client_capture')
|
|
->toContain('lpr_client_frame_width')
|
|
->toContain('lpr_client_frame_height')
|
|
->toContain('lpr_client_frame_bytes')
|
|
->toContain('lpr_local')
|
|
->toContain('lpr_payload')
|
|
->toContain('lpr_upstream_dns')
|
|
->toContain('lpr_upstream_connect')
|
|
->toContain('lpr_upstream')
|
|
->toContain('lpr_upstream_total')
|
|
->toContain('lpr_upstream_processing')
|
|
->toContain('lpr_total')
|
|
->toContain('lpr_route_total')
|
|
->toContain('lpr_request_total');
|
|
|
|
$capture = $fakePlateRecognizer->capture();
|
|
expect($capture['method'] ?? null)->toBe('POST');
|
|
expect($capture['authorization'] ?? null)->toBe('Token scanner-api-test-key');
|
|
expect($capture['expect'] ?? null)->toBeNull();
|
|
expect(json_decode((string)($capture['post']['config'] ?? ''), true))->toBe([
|
|
'mode' => 'fast',
|
|
'plates_per_vehicle' => 1,
|
|
'zoom_in_vehicles' => 0,
|
|
]);
|
|
expect($capture['post']['regions'] ?? null)->toBe('dk,de,se,no');
|
|
expect($capture['post']['client_capture_ms'] ?? null)->toBeNull();
|
|
expect($capture['post']['client_frame_width'] ?? null)->toBeNull();
|
|
expect($capture['post']['client_frame_height'] ?? null)->toBeNull();
|
|
expect($capture['post']['client_frame_bytes'] ?? null)->toBeNull();
|
|
expect($capture['files']['upload'] ?? [])->toMatchArray([
|
|
'name' => 'license-plate.jpg',
|
|
'type' => 'image/jpeg',
|
|
'size' => strlen('jpeg-camera-bytes'),
|
|
'error' => UPLOAD_ERR_OK,
|
|
'contents' => base64_encode('jpeg-camera-bytes'),
|
|
]);
|
|
} finally {
|
|
@unlink($imagePath);
|
|
$fakePlateRecognizer->stop();
|
|
scanner_lpr_api_set_env('PLATE_RECOGNIZER_API_URL', $previousPlateRecognizerUrl);
|
|
api_test_runtime()->restartServer();
|
|
}
|
|
});
|
|
|
|
final class ScannerLprFakePlateRecognizerServer
|
|
{
|
|
private mixed $process = null;
|
|
|
|
private function __construct(
|
|
private readonly string $directory,
|
|
private readonly string $capturePath,
|
|
private readonly int $port,
|
|
) {
|
|
}
|
|
|
|
public static function start(): self
|
|
{
|
|
$directory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'scanner-lpr-fake-' . bin2hex(random_bytes(6));
|
|
if (!mkdir($directory, 0777, true) && !is_dir($directory)) {
|
|
throw new RuntimeException('Unable to create fake Plate Recognizer directory.');
|
|
}
|
|
|
|
$capturePath = $directory . DIRECTORY_SEPARATOR . 'capture.json';
|
|
$routerPath = $directory . DIRECTORY_SEPARATOR . 'router.php';
|
|
file_put_contents($routerPath, <<<'PHP'
|
|
<?php
|
|
|
|
$path = parse_url((string)($_SERVER['REQUEST_URI'] ?? '/'), PHP_URL_PATH) ?: '/';
|
|
if ($path === '/health') {
|
|
header('Content-Type: text/plain');
|
|
echo 'ok';
|
|
return;
|
|
}
|
|
|
|
if ($path !== '/v1/plate-reader/') {
|
|
http_response_code(404);
|
|
header('Content-Type: application/json');
|
|
echo json_encode(['error' => 'not found']);
|
|
return;
|
|
}
|
|
|
|
$upload = $_FILES['upload'] ?? [];
|
|
$tmpName = is_array($upload) ? (string)($upload['tmp_name'] ?? '') : '';
|
|
$capture = [
|
|
'method' => $_SERVER['REQUEST_METHOD'] ?? null,
|
|
'authorization' => $_SERVER['HTTP_AUTHORIZATION'] ?? null,
|
|
'expect' => $_SERVER['HTTP_EXPECT'] ?? null,
|
|
'post' => $_POST,
|
|
'files' => [
|
|
'upload' => [
|
|
'name' => is_array($upload) ? ($upload['name'] ?? null) : null,
|
|
'type' => is_array($upload) ? ($upload['type'] ?? null) : null,
|
|
'size' => is_array($upload) ? ($upload['size'] ?? null) : null,
|
|
'error' => is_array($upload) ? ($upload['error'] ?? null) : null,
|
|
'contents' => $tmpName !== '' && is_file($tmpName) ? base64_encode((string)file_get_contents($tmpName)) : null,
|
|
],
|
|
],
|
|
];
|
|
|
|
file_put_contents((string)getenv('SCANNER_LPR_FAKE_CAPTURE_PATH'), json_encode($capture, JSON_UNESCAPED_SLASHES));
|
|
header('Content-Type: application/json');
|
|
echo json_encode([
|
|
'processing_time' => 58.184,
|
|
'results' => [['plate' => 'ab12345', 'score' => 0.98]],
|
|
], JSON_UNESCAPED_SLASHES);
|
|
PHP);
|
|
|
|
$port = ApiServer::findAvailablePort('127.0.0.1');
|
|
$server = new self($directory, $capturePath, $port);
|
|
$server->startProcess($routerPath);
|
|
$server->waitUntilReady();
|
|
|
|
return $server;
|
|
}
|
|
|
|
public function url(): string
|
|
{
|
|
return sprintf('http://127.0.0.1:%d', $this->port);
|
|
}
|
|
|
|
public function capture(): array
|
|
{
|
|
$contents = is_file($this->capturePath) ? file_get_contents($this->capturePath) : false;
|
|
expect($contents)->not->toBeFalse();
|
|
|
|
$decoded = json_decode((string)$contents, true);
|
|
expect($decoded)->toBeArray();
|
|
|
|
return $decoded;
|
|
}
|
|
|
|
public function stop(): void
|
|
{
|
|
if (is_resource($this->process)) {
|
|
proc_terminate($this->process);
|
|
usleep(250000);
|
|
$status = proc_get_status($this->process);
|
|
if (($status['running'] ?? false) && function_exists('posix_kill')) {
|
|
@posix_kill((int)$status['pid'], 9);
|
|
}
|
|
proc_close($this->process);
|
|
$this->process = null;
|
|
}
|
|
|
|
$this->removeDirectory($this->directory);
|
|
}
|
|
|
|
private function startProcess(string $routerPath): void
|
|
{
|
|
$command = sprintf(
|
|
'%s -S %s %s',
|
|
escapeshellarg((string)(PHP_BINARY ?: 'php')),
|
|
escapeshellarg('127.0.0.1:' . $this->port),
|
|
escapeshellarg($routerPath),
|
|
);
|
|
$environment = array_merge(getenv() ?: [], [
|
|
'SCANNER_LPR_FAKE_CAPTURE_PATH' => $this->capturePath,
|
|
]);
|
|
$descriptorSpec = [
|
|
0 => ['pipe', 'r'],
|
|
1 => ['file', $this->directory . DIRECTORY_SEPARATOR . 'stdout.log', 'a'],
|
|
2 => ['file', $this->directory . DIRECTORY_SEPARATOR . 'stderr.log', 'a'],
|
|
];
|
|
|
|
$this->process = proc_open($command, $descriptorSpec, $pipes, $this->directory, $environment);
|
|
if (!is_resource($this->process)) {
|
|
throw new RuntimeException('Unable to start fake Plate Recognizer server.');
|
|
}
|
|
|
|
if (isset($pipes[0]) && is_resource($pipes[0])) {
|
|
fclose($pipes[0]);
|
|
}
|
|
}
|
|
|
|
private function waitUntilReady(): void
|
|
{
|
|
$deadline = microtime(true) + 5;
|
|
$lastError = 'Timed out waiting for fake Plate Recognizer.';
|
|
|
|
while (microtime(true) < $deadline) {
|
|
$curl = curl_init($this->url() . '/health');
|
|
if ($curl === false) {
|
|
throw new RuntimeException('Unable to initialize fake Plate Recognizer health check.');
|
|
}
|
|
|
|
curl_setopt_array($curl, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_CONNECTTIMEOUT_MS => 250,
|
|
CURLOPT_TIMEOUT_MS => 500,
|
|
]);
|
|
$body = curl_exec($curl);
|
|
$status = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
|
if ($body === false) {
|
|
$lastError = curl_error($curl);
|
|
}
|
|
curl_close($curl);
|
|
|
|
if ($status === 200 && $body === 'ok') {
|
|
return;
|
|
}
|
|
|
|
usleep(100000);
|
|
}
|
|
|
|
throw new RuntimeException($lastError);
|
|
}
|
|
|
|
private function removeDirectory(string $directory): void
|
|
{
|
|
if (!is_dir($directory)) {
|
|
return;
|
|
}
|
|
|
|
foreach (glob($directory . DIRECTORY_SEPARATOR . '*') ?: [] as $path) {
|
|
if (is_file($path)) {
|
|
@unlink($path);
|
|
}
|
|
}
|
|
|
|
@rmdir($directory);
|
|
}
|
|
}
|
|
|
|
function scanner_lpr_api_get_env(string $key): ?string
|
|
{
|
|
$value = getenv($key);
|
|
|
|
return $value === false ? null : (string)$value;
|
|
}
|
|
|
|
function scanner_lpr_api_set_env(string $key, ?string $value): void
|
|
{
|
|
if ($value === null) {
|
|
putenv($key);
|
|
unset($_ENV[$key], $_SERVER[$key]);
|
|
|
|
return;
|
|
}
|
|
|
|
putenv($key . '=' . $value);
|
|
$_ENV[$key] = $value;
|
|
$_SERVER[$key] = $value;
|
|
}
|