Files
deskhub/DeskHub-Webseite/tests/mobile_api_integration.php
T
2026-10-16 19:12:24 +00:00

232 lines
9.4 KiB
PHP

<?php
declare(strict_types=1);
$root = dirname(__DIR__);
$sourceDb = $root . '/database/deskhub.sqlite';
$tempDb = tempnam(sys_get_temp_dir(), 'deskhub-api-');
if ($tempDb === false || !copy($sourceDb, $tempDb)) {
fwrite(STDERR, "Testdatenbank konnte nicht erstellt werden.\n");
exit(1);
}
$pdo = new PDO('sqlite:' . $tempDb);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('PRAGMA foreign_keys = ON');
$password = 'MobileApi-Test-Password-42';
$pdo->prepare('UPDATE staff SET password_hash = ?, is_active = 1 WHERE id = 1')
->execute([password_hash($password, PASSWORD_DEFAULT)]);
$agentPassword = 'MobileApi-Agent-Password-42';
$agentHash = password_hash($agentPassword, PASSWORD_DEFAULT);
$pdo->prepare(
"INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at)
VALUES ('Abteilungsagent', 'dept-agent@example.com', ?, 'agent', 2, 1, ?)"
)->execute([$agentHash, $now ?? date('Y-m-d H:i:s')]);
$pdo->prepare(
"INSERT INTO roles (name, permissions, created_at) VALUES ('Nur Antworten', '{\"reply_ticket\":true}', ?)"
)->execute([date('Y-m-d H:i:s')]);
$restrictedRoleId = (int) $pdo->lastInsertId();
$pdo->prepare(
"INSERT INTO staff (name, email, password_hash, role, role_id, department_id, is_active, created_at)
VALUES ('Eingeschränkter Agent', 'restricted-agent@example.com', ?, 'agent', ?, 1, 1, ?)"
)->execute([$agentHash, $restrictedRoleId, date('Y-m-d H:i:s')]);
$number = 'API-' . random_int(100000, 999999);
$now = date('Y-m-d H:i:s');
$pdo->prepare(
"INSERT INTO tickets
(ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, created_at, updated_at)
VALUES (?,?,?,?,?,?,'open','web',?,?)"
)->execute([$number, 'Mobiler Integrationstest', 'Test Kunde', 'kunde@example.test', 1, 2, $now, $now]);
$ticketId = (int) $pdo->lastInsertId();
$pdo->prepare(
"INSERT INTO ticket_threads (ticket_id, author_type, message, is_internal_note, created_at)
VALUES (?,'customer',?,0,?)"
)->execute([$ticketId, 'Bitte mobil bearbeiten.', $now]);
$port = random_int(18080, 18999);
$command = [PHP_BINARY, '-S', "127.0.0.1:$port", '-t', $root];
$previousDbPath = getenv('DESKHUB_SQLITE_PATH');
$previousInsecure = getenv('DESKHUB_API_ALLOW_INSECURE');
putenv('DESKHUB_SQLITE_PATH=' . $tempDb);
putenv('DESKHUB_API_ALLOW_INSECURE=1');
$process = proc_open(
$command,
[['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']],
$pipes,
$root,
null,
);
putenv($previousDbPath === false ? 'DESKHUB_SQLITE_PATH' : 'DESKHUB_SQLITE_PATH=' . $previousDbPath);
putenv($previousInsecure === false ? 'DESKHUB_API_ALLOW_INSECURE' : 'DESKHUB_API_ALLOW_INSECURE=' . $previousInsecure);
if (!is_resource($process)) {
unlink($tempDb);
fwrite(STDERR, "PHP-Testserver konnte nicht gestartet werden.\n");
exit(1);
}
try {
$ready = false;
for ($attempt = 0; $attempt < 30; $attempt++) {
$socket = @fsockopen('127.0.0.1', $port, $errno, $error, 0.1);
if ($socket) {
fclose($socket);
$ready = true;
break;
}
usleep(100000);
}
if (!$ready) {
$stderr = stream_get_contents($pipes[2]);
throw new RuntimeException('PHP-Testserver ist nicht erreichbar. ' . trim($stderr));
}
[$status] = request($port, 'GET', '/api/staff/v1/me.php');
assert_same(401, $status, 'Geschützte Endpunkte müssen ohne Token 401 liefern.');
[$status, $login] = request($port, 'POST', '/api/staff/v1/login.php', [
'email' => 'admin@example.com',
'password' => $password,
'deviceName' => 'Integrationstest',
]);
assert_same(200, $status, 'Mitarbeiter-Login muss erfolgreich sein.');
$token = $login['token'] ?? '';
assert_true(strlen($token) >= 40, 'Login muss ein starkes Bearer-Token liefern.');
$headers = ['Authorization: Bearer ' . $token];
[$status, $metadata] = request($port, 'GET', '/api/staff/v1/metadata.php', null, $headers);
assert_same(200, $status, 'Metadaten müssen abrufbar sein.');
assert_same('admin@example.com', $metadata['staff']['email'] ?? null, 'Falsches Mitarbeiterprofil.');
[$status, $tickets] = request(
$port,
'GET',
'/api/staff/v1/tickets.php?q=' . rawurlencode('Mobiler Integrationstest'),
null,
$headers,
);
assert_same(200, $status, 'Ticketliste muss abrufbar sein.');
assert_same($number, $tickets['items'][0]['number'] ?? null, 'Ticketsuche liefert nicht das Testticket.');
[, $departmentLogin] = request($port, 'POST', '/api/staff/v1/login.php', [
'email' => 'dept-agent@example.com',
'password' => $agentPassword,
'deviceName' => 'Abteilungstest',
]);
$departmentHeaders = ['Authorization: Bearer ' . ($departmentLogin['token'] ?? '')];
[$status] = request(
$port,
'GET',
'/api/staff/v1/ticket.php?number=' . rawurlencode($number),
null,
$departmentHeaders,
);
assert_same(403, $status, 'Abteilungsfremde Tickets müssen serverseitig gesperrt sein.');
[, $restrictedLogin] = request($port, 'POST', '/api/staff/v1/login.php', [
'email' => 'restricted-agent@example.com',
'password' => $agentPassword,
'deviceName' => 'Rollentest',
]);
$restrictedHeaders = ['Authorization: Bearer ' . ($restrictedLogin['token'] ?? '')];
[$status] = request($port, 'POST', '/api/staff/v1/ticket_message.php', [
'number' => $number,
'type' => 'note',
'message' => 'Diese Notiz darf nicht gespeichert werden.',
], $restrictedHeaders);
assert_same(403, $status, 'Fehlende Rollenrechte müssen serverseitig gesperrt sein.');
[$status] = request($port, 'POST', '/api/staff/v1/ticket_message.php', [
'number' => $number,
'type' => 'reply',
'message' => 'Diese Antwort darf das Ticket nicht schließen.',
'setStatus' => 'closed',
], $restrictedHeaders);
assert_same(403, $status, 'Antwortrechte dürfen das Recht zum Schließen nicht umgehen.');
[$status] = request($port, 'POST', '/api/staff/v1/ticket_message.php', [
'number' => $number,
'type' => 'reply',
'message' => 'Öffentliche Antwort aus dem Integrationstest',
'setStatus' => 'answered',
], $restrictedHeaders);
assert_same(201, $status, 'Erlaubte öffentliche Antworten müssen erstellt werden.');
[$status, $detail] = request(
$port,
'GET',
'/api/staff/v1/ticket.php?number=' . rawurlencode($number),
null,
$headers,
);
assert_same(200, $status, 'Ticketdetail muss abrufbar sein.');
assert_same('Bitte mobil bearbeiten.', $detail['ticket']['threads'][0]['message'] ?? null, 'Thread fehlt.');
[$status, $updated] = request($port, 'PATCH', '/api/staff/v1/ticket_update.php', [
'number' => $number,
'status' => 'pending',
'assignedStaffId' => 1,
], $headers);
assert_same(200, $status, 'Status und Zuständigkeit müssen änderbar sein.');
assert_same('pending', $updated['ticket']['status'] ?? null, 'Status wurde nicht gespeichert.');
[$status] = request($port, 'POST', '/api/staff/v1/ticket_message.php', [
'number' => $number,
'type' => 'note',
'message' => 'Interne Notiz aus dem Integrationstest',
], $headers);
assert_same(201, $status, 'Interne Notiz muss erstellt werden.');
[$status] = request($port, 'POST', '/api/staff/v1/logout.php', [], $headers);
assert_same(200, $status, 'Logout muss erfolgreich sein.');
[$status] = request($port, 'GET', '/api/staff/v1/me.php', null, $headers);
assert_same(401, $status, 'Widerrufenes Token darf nicht weiter funktionieren.');
$check = new PDO('sqlite:' . $tempDb);
$stmt = $check->prepare('SELECT COUNT(*) FROM ticket_threads WHERE ticket_id = ? AND is_internal_note = 1');
$stmt->execute([$ticketId]);
assert_same(1, (int) $stmt->fetchColumn(), 'Interne Notiz fehlt in der Datenbank.');
echo "Mobile API integration test passed.\n";
} finally {
proc_terminate($process);
foreach ($pipes as $pipe) {
if (is_resource($pipe)) fclose($pipe);
}
proc_close($process);
@unlink($tempDb);
}
function request(int $port, string $method, string $path, ?array $body = null, array $headers = []): array
{
$headers[] = 'Accept: application/json';
$content = null;
if ($body !== null) {
$content = json_encode($body, JSON_THROW_ON_ERROR);
$headers[] = 'Content-Type: application/json';
}
$context = stream_context_create([
'http' => [
'method' => $method,
'header' => implode("\r\n", $headers),
'content' => $content,
'ignore_errors' => true,
'timeout' => 10,
],
]);
$raw = file_get_contents("http://127.0.0.1:$port$path", false, $context);
$statusLine = $http_response_header[0] ?? '';
preg_match('/\s(\d{3})\s/', $statusLine, $matches);
$status = (int) ($matches[1] ?? 0);
$decoded = $raw !== false && $raw !== '' ? json_decode($raw, true) : [];
return [$status, is_array($decoded) ? $decoded : []];
}
function assert_true(bool $condition, string $message): void
{
if (!$condition) throw new RuntimeException($message);
}
function assert_same(mixed $expected, mixed $actual, string $message): void
{
if ($expected !== $actual) {
throw new RuntimeException($message . ' Erwartet: ' . var_export($expected, true)
. ', erhalten: ' . var_export($actual, true));
}
}