Files
2026-10-24 20:14:43 +00:00

188 lines
12 KiB
PHP

<?php
declare(strict_types=1);
/**
* Prüft das Plugin "Störungsmitteilung und Rundmail": Empfängergruppen (offene Tickets, Abteilung, Organisation), Filter für unbrauchbare
* Adressen, Rechte der Mitarbeiter, Versand in Stapeln mit Protokoll, Abbruch, Begrenzungen und Probemail.
*/
$pluginDir = sys_get_temp_dir() . '/deskhub-rm-' . bin2hex(random_bytes(4));
mkdir($pluginDir, 0755, true);
putenv('DESKHUB_PLUGINS_DIR=' . $pluginDir);
putenv('DESKHUB_TESTING=1');
putenv('DESKHUB_CATALOG_DEFAULTS=' . __DIR__ . '/fixtures/no-catalog.php');
putenv('DESKHUB_SQLITE_PATH=:memory:');
require_once dirname(__DIR__) . '/includes/bootstrap.php';
$failures = [];
$check = function (bool $ok, string $what) use (&$failures): void {
if (!$ok) $failures[] = $what;
};
$pdo = get_db();
require_once __DIR__ . '/fixtures/paths.php';
$src = plugin_source_dir_or_skip('rundmail', 'plugin_rundmail');
$zipPath = sys_get_temp_dir() . '/rm-' . bin2hex(random_bytes(3)) . '.zip';
$zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::CREATE);
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($src, FilesystemIterator::SKIP_DOTS)) as $f) {
$zip->addFile($f->getPathname(), str_replace('\\', '/', substr($f->getPathname(), strlen($src) + 1)));
}
$zip->close();
$r = plugin_install_zip($pdo, $zipPath);
$check($r['ok'] && $r['slug'] === 'rundmail', 'Plugin lässt sich installieren: ' . json_encode($r));
$act = plugin_set_active($pdo, 'rundmail', true, ['hooks.cron', 'ui.menu', 'db.migrations']);
$check($act['ok'], 'Plugin lässt sich aktivieren: ' . json_encode($act));
$state = &plugin_state();
$state['booted'] = false;
plugins_boot();
$check(count(plugin_menu_items(['id' => 1, 'role' => 'agent'])) === 1, 'Menüpunkt ist für Mitarbeiter sichtbar');
$sentMails = [];
$GLOBALS['rundmail_send'] = function (string $to, string $subject, string $html) use (&$sentMails): bool {
if (str_contains($to, 'kaputt')) return false;
$sentMails[] = compact('to', 'subject', 'html');
return true;
};
$d1 = (int) $pdo->query('SELECT id FROM departments ORDER BY id LIMIT 1')->fetchColumn();
$pdo->prepare("INSERT INTO departments (name, email, is_active, created_at) VALUES ('Technik', 'technik@team.example', 1, ?)")->execute([db_now()]);
$d2 = (int) $pdo->lastInsertId();
$prio = (int) $pdo->query('SELECT id FROM priorities ORDER BY sort_order LIMIT 1')->fetchColumn();
$mkStaff = function (string $name, string $role, ?int $dept) use ($pdo): array {
$pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)')
->execute([$name, strtolower($name) . '@team.example', 'x', $role, $dept, db_now()]);
return ['id' => (int) $pdo->lastInsertId(), 'role' => $role, 'department_id' => $dept, 'name' => $name, 'email' => strtolower($name) . '@team.example'];
};
$anna = $mkStaff('Anna', 'agent', $d1);
$chef = $mkStaff('Chef', 'admin', null);
$pdo->prepare("INSERT INTO organizations (name, created_at) VALUES ('Muster GmbH', ?)")->execute([db_now()]);
$org = (int) $pdo->lastInsertId();
$mkUser = function (string $name, string $email, ?int $org) use ($pdo): int {
$pdo->prepare('INSERT INTO users (name, email, password_hash, organization_id, created_at) VALUES (?,?,?,?,?)')->execute([$name, $email, 'x', $org, db_now()]);
return (int) $pdo->lastInsertId();
};
$u1 = $mkUser('Erika Muster', 'erika@muster.example', $org);
$u2 = $mkUser('Max Muster', 'max@muster.example', $org);
$n = 0;
$mk = function (string $name, string $email, int $dept, string $status = 'open', ?int $userId = null, int $spam = 0) use ($pdo, $prio, &$n): int {
$n++;
$now = db_now();
$pdo->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, user_id, department_id, priority_id, status, source, is_spam, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)')
->execute(['RM-' . $n, 'T' . $n, $name, $email, $userId, $dept, $prio, $status, 'web', $spam, $now, $now]);
return (int) $pdo->lastInsertId();
};
$mk('Erika Muster', 'erika@muster.example', $d1, 'open', $u1);
$mk('Erika Muster', 'ERIKA@muster.example', $d1, 'pending', $u1); // gleiche Adresse, einmal
$mk('Gast Eins', 'gast1@kunde.example', $d1);
$mk('Gast Zwei', 'gast2@kunde.example', $d2);
$mk('Fertig', 'fertig@kunde.example', $d1, 'closed');
$mk('Gelöst', 'geloest@kunde.example', $d1, 'resolved');
$mk('Spammer', 'spam@kunde.example', $d1, 'open', null, 1);
$mk('Discord', 'discord-123@discord.invalid', $d1);
$mk('Anonym', 'anon_abcd@deleted.local', $d1);
$mk('Kaputt', 'kaputt@kunde.example', $d1);
$pdo->prepare('INSERT INTO banlist (match_type, value, created_at) VALUES (?,?,?)')->execute(['email', 'gesperrt@kunde.example', db_now()]);
$mk('Gesperrt', 'gesperrt@kunde.example', $d1);
$mails = fn(array $rows) => array_map(fn($r) => $r['email'], $rows);
// Empfänger
$all = $mails(rundmail_recipients($pdo, $chef, 'open', null));
sort($all);
$check($all === ['erika@muster.example', 'gast1@kunde.example', 'gast2@kunde.example', 'kaputt@kunde.example'], 'offene Tickets: nur brauchbare Adressen, einmal je Adresse: ' . json_encode($all));
$dep = $mails(rundmail_recipients($pdo, $chef, 'department', $d2));
$check($dep === ['gast2@kunde.example'], 'Abteilung: nur offene Tickets dieser Abteilung');
$orgAll = $mails(rundmail_recipients($pdo, $chef, 'organization', $org));
$check($orgAll === ['erika@muster.example', 'max@muster.example'], 'Organisation (Administrator): alle Mitglieder');
$orgStaff = $mails(rundmail_recipients($pdo, $anna, 'organization', $org));
$check($orgStaff === ['erika@muster.example'], 'Organisation (Mitarbeiter): nur Mitglieder mit offenem Ticket in der eigenen Abteilung');
$annaOpen = $mails(rundmail_recipients($pdo, $anna, 'open', null));
sort($annaOpen);
$check($annaOpen === ['erika@muster.example', 'gast1@kunde.example', 'kaputt@kunde.example'], 'Mitarbeiter sehen nur Kunden der eigenen Abteilung');
$check(rundmail_recipients($pdo, $anna, 'department', $d2) === [], 'fremde Abteilung ist für Mitarbeiter leer');
$groupsAnna = array_column(rundmail_groups($pdo, $anna), 'key');
$groupsChef = array_column(rundmail_groups($pdo, $chef), 'key');
$check(!in_array('department:' . $d2, $groupsAnna, true) && in_array('department:' . $d2, $groupsChef, true) && in_array('organization:' . $org, $groupsAnna, true), 'Gruppenauswahl richtet sich nach den Abteilungen');
$check(rundmail_parse_group($pdo, 'department:99999') === null && rundmail_parse_group($pdo, 'x') === null && rundmail_parse_group($pdo, 'open')['type'] === 'open', 'Gruppenschlüssel werden geprüft');
// Prüfungen
$check(rundmail_validate('', 'Text') !== null && rundmail_validate('Betreff', '') !== null && rundmail_validate("A\nB", 'x') !== null && rundmail_validate(str_repeat('x', 151), 'x') !== null, 'Betreff und Text werden geprüft');
$check(!rundmail_create($pdo, $anna, 'department:' . $d2, 'Hallo', 'Text')['ok'], 'Mitarbeiter können nicht an fremde Abteilungen senden');
$check(!rundmail_create($pdo, $chef, 'open', '', 'Text')['ok'], 'ohne Betreff kein Versand');
$check(!rundmail_create($pdo, $chef, 'organization:99999', 'Hallo', 'Text')['ok'], 'unbekannte Organisation');
// Rendern
[$subj, $html] = rundmail_render(['subject' => 'Wartung', 'body' => "Hallo {name},\n<b>fett</b>"], 'Erika <script>');
$check($subj === 'Wartung' && str_contains($html, 'Hallo Erika &lt;script&gt;') && !str_contains($html, '<script>') && !str_contains($html, '<b>fett') && str_contains($html, '<br'), 'Text wird maskiert, Name eingesetzt, Zeilenumbrüche bleiben');
[, $anon] = rundmail_render(['subject' => 'x', 'body' => 'Hallo {name}'], '');
$check(str_contains($anon, 'Hallo Kunde'), 'ohne Namen steht „Kunde“');
// Versand in Stapeln (Admin: 4 Empfänger, davon einer mit Fehler)
$res = rundmail_create($pdo, $chef, 'open', 'Aktuelle Störung', 'Hallo {name}, wir arbeiten daran.');
$check($res['ok'] && $res['total'] === 4, 'Rundmail wird angelegt: ' . json_encode($res));
$msg = $pdo->query("SELECT * FROM plugin_rundmail_messages WHERE id = {$res['id']}")->fetch();
$check($msg['status'] === 'done' && (int) $msg['sent'] === 3 && (int) $msg['failed'] === 1, 'kleine Gruppe geht sofort raus, der Fehler wird gezählt: ' . json_encode($msg));
$sentTo = array_column($sentMails, 'to');
sort($sentTo);
$check($sentTo === ['erika@muster.example', 'gast1@kunde.example', 'gast2@kunde.example'], 'jeder Empfänger bekommt eine eigene Mail (keine Sammelmail)');
$check(str_contains(array_values(array_filter($sentMails, fn($m) => $m['to'] === 'erika@muster.example'))[0]['html'], 'Hallo Erika Muster'), 'Name wird je Empfänger eingesetzt');
$err = $pdo->query("SELECT error FROM plugin_rundmail_queue WHERE email = 'kaputt@kunde.example'")->fetchColumn();
$check(str_contains((string) $err, 'fehlgeschlagen'), 'Fehler steht im Protokoll');
// Große Gruppe: Stapel und Hintergrundlauf
$bulk = [];
for ($i = 0; $i < 60; $i++) $bulk[] = $mk('Kunde ' . $i, "bulk{$i}@kunde.example", $d2);
$sentMails = [];
$res2 = rundmail_create($pdo, $chef, 'department:' . $d2, 'Wartung', 'Hallo {name}');
$check($res2['ok'] && $res2['total'] === 61 && count($sentMails) === 25, 'erster Stapel: 25 Mails sofort');
$st = $pdo->query("SELECT status, sent FROM plugin_rundmail_messages WHERE id = {$res2['id']}")->fetch();
$check($st['status'] === 'sending' && (int) $st['sent'] === 25, 'Nachricht steht auf „Läuft“');
$check(rundmail_tick($pdo) === 25 && count($sentMails) === 50, 'Hintergrundlauf verschickt den nächsten Stapel');
$check(rundmail_tick($pdo) === 11 && count($sentMails) === 61, 'letzter Stapel');
$st = $pdo->query("SELECT status, sent, failed, finished_at FROM plugin_rundmail_messages WHERE id = {$res2['id']}")->fetch();
$check($st['status'] === 'done' && (int) $st["sent"] === 61 && $st["finished_at"] !== null, 'Nachricht ist fertig');
$check(rundmail_tick($pdo) === 0, 'danach ist nichts mehr zu tun');
// Abbruch
$sentMails = [];
$res3 = rundmail_create($pdo, $chef, 'department:' . $d2, 'Noch eine', 'Hallo');
$check(!rundmail_cancel($pdo, $anna, $res3['id']), 'Mitarbeiter können fremde Versände nicht abbrechen');
$check(rundmail_cancel($pdo, $chef, $res3['id']), 'Administrator bricht ab');
$check(rundmail_tick($pdo) === 0 && count($sentMails) === 25, 'nach dem Abbruch geht nichts mehr raus');
$st = $pdo->query("SELECT status, sent FROM plugin_rundmail_messages WHERE id = {$res3['id']}")->fetch();
$check($st['status'] === 'cancelled' && (int) $st['sent'] === 25, 'Stand nach Abbruch: abgebrochen, 25 gesendet');
$check(!rundmail_cancel($pdo, $chef, $res3['id']), 'Fertige oder abgebrochene Versände lassen sich nicht noch einmal abbrechen');
// Begrenzungen für Mitarbeiter
$pdo->exec("DELETE FROM plugin_rundmail_messages");
$pdo->exec("DELETE FROM plugin_rundmail_queue");
for ($i = 0; $i < 250; $i++) $mk('Viele ' . $i, "viele{$i}@kunde.example", $d1);
$tooMany = rundmail_create($pdo, $anna, 'open', 'Viele', 'Text');
$check(!$tooMany['ok'] && str_contains($tooMany['error'], 'höchstens 200'), 'Mitarbeiter dürfen höchstens 200 Empfänger: ' . json_encode($tooMany));
$pdo->exec("DELETE FROM tickets WHERE ticket_number LIKE 'RM-%' AND requester_email LIKE 'viele%'");
$pdo->exec("DELETE FROM tickets WHERE requester_email LIKE 'bulk%'");
for ($i = 0; $i < 5; $i++) rundmail_create($pdo, $anna, 'open', 'Hallo ' . $i, 'Text');
$limited = rundmail_create($pdo, $anna, 'open', 'Sechste', 'Text');
$check(!$limited['ok'] && str_contains($limited['error'], 'letzten Stunde'), 'höchstens 5 Rundmails pro Stunde für Mitarbeiter');
$check(count(rundmail_history($pdo, $anna)) === 5 && count(rundmail_history($pdo, $chef)) === 5, 'Verlauf: Mitarbeiter sehen ihre eigenen, Administratoren alle');
$check(count(rundmail_history($pdo, $mkStaff('Ben', 'agent', $d1))) === 0, 'fremde Nachrichten sehen Mitarbeiter nicht');
// Probemail
$sentMails = [];
$t = rundmail_test($anna, 'Wartung', 'Hallo {name}');
$check($t['ok'] && $sentMails[0]['to'] === 'anna@team.example' && str_starts_with($sentMails[0]['subject'], '[TEST]') && str_contains($sentMails[0]['html'], 'Hallo Anna'), 'Probemail geht an den Mitarbeiter');
$check(!rundmail_test($anna, '', 'x')['ok'], 'Probemail prüft die Eingaben');
unset($GLOBALS['rundmail_send']);
foreach (scandir($pluginDir) ?: [] as $entry) {
if ($entry === '.' || $entry === '..') continue;
is_dir($pluginDir . '/' . $entry) ? plugin_rmdir($pluginDir . '/' . $entry) : @unlink($pluginDir . '/' . $entry);
}
@rmdir($pluginDir);
@unlink($zipPath);
if ($failures) {
fwrite(STDERR, "Fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n");
exit(1);
}
echo 'Rundmail plugin checks passed.' . "\n";