274 lines
13 KiB
PHP
274 lines
13 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) {
|
|
fwrite(STDERR, "Testdatenbank konnte nicht erstellt werden.\n");
|
|
exit(1);
|
|
}
|
|
if (is_file($sourceDb)) {
|
|
copy($sourceDb, $tempDb);
|
|
} else {
|
|
// Keine Entwicklungsdatenbank vorhanden (frische Kopie, CI): Datenbank aus Schema und Migrationen aufbauen
|
|
require_once $root . '/includes/db.php';
|
|
require_once $root . '/database/migrate.php';
|
|
$fresh = new PDO('sqlite:' . $tempDb);
|
|
$fresh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$fresh->exec('PRAGMA foreign_keys = ON');
|
|
$fresh->exec(convert_schema_to_sqlite((string) file_get_contents($root . '/database/schema.sql')));
|
|
$migrated = run_migrations($fresh, true);
|
|
if ($migrated['errors']) {
|
|
fwrite(STDERR, 'Testdatenbank konnte nicht aufgebaut werden: ' . implode('; ', $migrated['errors']) . "\n");
|
|
exit(1);
|
|
}
|
|
$fresh = null;
|
|
}
|
|
|
|
$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]);
|
|
|
|
// Signaturen (Mitarbeiter und Abteilung 1) und Textbausteine für zwei Abteilungen
|
|
$pdo->exec("UPDATE departments SET signature = '<p>Abteilungs-Team<br><img src=\"https://example.org/logo.png\"></p>' WHERE id = 1");
|
|
$pdo->exec("UPDATE staff SET signature = '<p>Viele Grüße<br>Admin persönlich</p>' WHERE email = 'admin@example.com'");
|
|
$pdo->exec("INSERT INTO departments (name, email, is_default) VALUES ('Zweite Abteilung', 'zweite@example.org', 0)");
|
|
$secondDeptId = (int) $pdo->lastInsertId();
|
|
$pdo->exec('DELETE FROM canned_responses');
|
|
$cannedIns = $pdo->prepare('INSERT INTO canned_responses (title, body, department_id, created_at) VALUES (?,?,?,?)');
|
|
$cannedIns->execute(['Nur Abteilung eins', 'A', 1, $now]);
|
|
$cannedIns->execute(['Nur Abteilung zwei', 'B', $secondDeptId, $now]);
|
|
$cannedIns->execute(['Für alle', 'C', null, $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.');
|
|
|
|
// Antwort-Signaturen im Ticket und Textbausteine nach Abteilung (für Windows- und Android-App)
|
|
[$status, $ticketData] = request($port, 'GET', '/api/staff/v1/ticket.php?number=' . rawurlencode($number), null, $headers);
|
|
$rs = $ticketData['ticket']['replySignatures'] ?? [];
|
|
assert_same(200, $status, 'Ticket muss abrufbar sein.');
|
|
assert_same('own', $rs['default'] ?? null, 'Voreinstellung: eigene Signatur, wenn vorhanden.');
|
|
assert_true(($rs['own']['available'] ?? false) === true && str_contains((string) ($rs['own']['text'] ?? ''), 'Admin persönlich'), 'Eigene Signatur kommt als Klartext.');
|
|
assert_true(($rs['department']['available'] ?? false) === true && str_contains((string) ($rs['department']['text'] ?? ''), 'Abteilungs-Team') && ($rs['department']['hasImage'] ?? false) === true, 'Abteilungs-Signatur mit Hinweis auf Bild.');
|
|
assert_true(($rs['own']['hasImage'] ?? true) === false && !str_contains((string) ($rs['own']['text'] ?? ''), '<'), 'Klartext enthält kein HTML.');
|
|
[$status, $cannedAll] = request($port, 'GET', '/api/staff/v1/canned_responses.php', null, $headers);
|
|
assert_same(3, count($cannedAll['items'] ?? []), 'Ohne Filter: alle Textbausteine.');
|
|
[$status, $cannedDept] = request($port, 'GET', '/api/staff/v1/canned_responses.php?departmentId=1', null, $headers);
|
|
$titles = array_column($cannedDept['items'] ?? [], 'title');
|
|
assert_true(in_array('Nur Abteilung eins', $titles, true) && in_array('Für alle', $titles, true) && !in_array('Nur Abteilung zwei', $titles, true), 'Mit departmentId: Textbausteine dieser Abteilung und die für alle, keine fremden.');
|
|
$pdo->exec('UPDATE tickets SET locked_by = NULL, locked_at = NULL'); // das Abrufen oben hat die Ticket-Sperre des Admins gesetzt, die spätere Tests nicht stören soll
|
|
[, $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));
|
|
}
|
|
}
|