188 lines
12 KiB
PHP
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 <script>') && !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";
|