added unit tests

This commit is contained in:
2026-06-04 21:40:59 +02:00
parent d80a8de559
commit 48a619ee4b
64 changed files with 3807 additions and 33 deletions

181
tests/Support/ApiClient.php Normal file
View File

@ -0,0 +1,181 @@
<?php
declare(strict_types=1);
namespace Tests\Support;
/**
* Dispatches requests through api/index.php (same as production routing).
*/
final class ApiClient
{
public function __construct(
private readonly string $apiIndexPath,
) {
}
/**
* @param array<string, string> $headers
* @return array{ok: bool, status: int, data?: mixed, error?: array<string, mixed>}
*/
public function request(
string $method,
string $route,
?array $body = null,
array $headers = [],
array $query = [],
): array {
qdb_test_reset_http_state();
$_SERVER['REQUEST_METHOD'] = strtoupper($method);
$queryString = $query !== [] ? '?' . http_build_query($query) : '';
$_SERVER['REQUEST_URI'] = '/api/' . ltrim($route, '/') . $queryString;
$_SERVER['SCRIPT_NAME'] = '/api/index.php';
$_GET = $query;
if ($queryString !== '') {
parse_str(ltrim($queryString, '?'), $_GET);
}
unset($_SERVER['HTTP_AUTHORIZATION'], $_SERVER['Authorization']);
unset($_SERVER['HTTP_X_QDB_CLIENT']);
foreach ($headers as $name => $value) {
$key = 'HTTP_' . strtoupper(str_replace('-', '_', $name));
$_SERVER[$key] = $value;
}
if ($body !== null) {
qdb_test_set_request_body(json_encode($body, JSON_THROW_ON_ERROR));
}
ob_start();
try {
require $this->apiIndexPath;
ob_end_clean();
return ['ok' => false, 'status' => 500, 'error' => ['code' => 'NO_RESPONSE', 'message' => 'Handler did not return JSON']];
} catch (JsonSuccessResponse $e) {
$output = ob_get_clean();
return [
'ok' => true,
'status' => $e->httpStatus,
'data' => $e->data,
'_raw' => $output,
];
} catch (JsonErrorResponse $e) {
ob_get_clean();
$err = ['code' => $e->errorCode, 'message' => $e->errorMessage];
if ($e->details !== null) {
$err['details'] = $e->details;
}
return [
'ok' => false,
'status' => $e->httpStatus,
'error' => $err,
];
} catch (RawHttpResponse $e) {
ob_get_clean();
return [
'ok' => true,
'status' => $e->httpStatus,
'raw' => true,
'body' => $e->body,
'headers' => $e->headers,
];
} catch (\Throwable $e) {
ob_end_clean();
throw $e;
}
}
public function loginWeb(string $username, string $password): array
{
return $this->request('POST', 'auth/login', [
'username' => $username,
'password' => $password,
], ['X-QDB-Client' => 'web']);
}
public function loginMobile(string $username, string $password): array
{
return $this->request('POST', 'auth/login', [
'username' => $username,
'password' => $password,
]);
}
/**
* @return array{token: string, data: array<string, mixed>}
*/
public function loginMobileAndGetToken(string $username, string $password): array
{
$res = $this->loginMobile($username, $password);
if (!$res['ok']) {
throw new \RuntimeException('Login failed: ' . ($res['error']['message'] ?? 'unknown'));
}
$token = $res['data']['token'] ?? '';
if ($token === '') {
throw new \RuntimeException('Login response missing token');
}
return ['token' => $token, 'data' => $res['data']];
}
/**
* @return array{token: string, data: array<string, mixed>}
*/
public function loginWebAndGetToken(string $username, string $password): array
{
$res = $this->loginWeb($username, $password);
if (!$res['ok']) {
throw new \RuntimeException('Login failed: ' . ($res['error']['message'] ?? 'unknown'));
}
$token = $res['data']['token'] ?? '';
if ($token === '') {
throw new \RuntimeException('Login response missing token');
}
return ['token' => $token, 'data' => $res['data']];
}
/**
* @param array<string, string> $extraHeaders
*/
public function withToken(string $token, string $method, string $route, ?array $body = null, array $query = [], array $extraHeaders = []): array
{
$headers = array_merge([
'Authorization' => 'Bearer ' . $token,
'X-QDB-Client' => 'web',
], $extraHeaders);
return $this->request($method, $route, $body, $headers, $query);
}
/**
* @param array<string, string> $extraHeaders
*/
public function withMobileToken(
string $token,
string $method,
string $route,
?array $body = null,
array $query = [],
array $extraHeaders = [],
): array {
$headers = array_merge(['Authorization' => 'Bearer ' . $token], $extraHeaders);
return $this->request($method, $route, $body, $headers, $query);
}
/**
* POST an encrypted payload (Android app API).
*
* @param array<string, mixed> $payload
*/
public function encryptedPost(string $token, string $route, array $payload): array
{
$envelope = qdb_sensitive_envelope(
json_encode($payload, JSON_THROW_ON_ERROR),
$token
);
return $this->withMobileToken($token, 'POST', $route, $envelope);
}
}