'Landingpage', 'path' => 'landing.php', 'contains' => ['Kaffeeliste', 'Kundenkonto anlegen'], ], [ 'label' => 'Impressum', 'path' => 'impressum.php', 'contains' => ['Impressum', 'In den Sieben Stücken 9d', '30655 Hannover'], ], [ 'label' => 'AGB', 'path' => 'agb.php', 'contains' => ['Allgemeine Geschäftsbedingungen', 'Entwurf'], ], [ 'label' => 'Preise', 'path' => 'preise.php', 'contains' => ['Preise', '3,99', '7,99', '12,99', '§ 19 UStG'], ], [ 'label' => 'Datenschutzerklärung', 'path' => 'datenschutz.php', 'contains' => ['Datenschutzerklärung', 'Verantwortlicher', 'Auftragsverarbeiter'], ], [ 'label' => 'Dashboard', 'path' => 'index.php', 'auth' => true, 'contains' => ['Aktueller Stand', 'Jahresübersicht', 'Letzte Einzahlungen'], ], [ 'label' => 'Striche erfassen', 'path' => 'stricheintragen.php', 'auth' => true, 'contains' => ['Anzahl der Striche'], ], [ 'label' => 'Einzahlungen erfassen', 'path' => 'einzahlung.php', 'auth' => true, 'contains' => ['Einzahlungen', 'Bemerkung'], ], [ 'label' => 'Kaffeeliste', 'path' => 'kaffeeliste.php', 'auth' => true, 'contains' => [ 'Aktive Mitarbeiter', 'teilnehmerauswertung.php?participant_id=', 'Smoke Guthaben', $erwartetGuthaben, 'Smoke Schulden', $erwartetSchulden, 'Smoke Ohne Buchungen', '0,00 €', ], 'not_contains' => ['Smoke Inaktiv'], ], [ 'label' => 'Ledger Preview', 'path' => 'ledger-preview.php', 'auth' => true, 'contains' => ['Journal-Vorschau', 'Tenant-Summen', 'Letzte Ledger-Buchungen'], ], [ 'label' => 'Mitgliederverwaltung', 'path' => 'mitarbeiterverwalten.php', 'auth' => true, 'contains' => ['Mitglieder verwalten', 'PayPal-Name'], ], [ 'label' => 'Letzte Einträge', 'path' => 'letzteneintraege.php', 'auth' => true, 'contains' => ['Letzte 100 Einzahlungen', 'Stornieren'], ], [ 'label' => 'CSV-Upload', 'path' => 'csvupload.php', 'auth' => true, 'contains' => ['CSV-Import'], ], [ 'label' => 'Hinweise', 'path' => 'hinweise.php', 'auth' => true, 'contains' => ['Kaffeeliste - Hinweise'], ], [ 'label' => 'FAQ', 'path' => 'faq.php', 'auth' => true, 'contains' => ['FAQ - Kaffeeliste'], ], [ 'label' => 'Login', 'path' => 'login.php', 'contains' => ['Login', 'Registrieren', 'Passwort vergessen'], ], [ 'label' => 'Registrierung', 'path' => 'register.php', 'contains' => ['Registrierung', 'Kundenkonto anlegen'], ], [ 'label' => 'Passwort vergessen', 'path' => 'passwort-vergessen.php', 'contains' => ['Passwort vergessen', 'Link vorbereiten'], ], [ 'label' => 'Mandantenauswahl Login-Schutz', 'path' => 'mandant-auswahl.php', 'expect_redirect' => 'login.php', 'contains' => [], ], [ 'label' => 'Passwort zurücksetzen Invalid', 'path' => 'passwort-zuruecksetzen.php', 'contains' => ['Passwort zurücksetzen', 'ungültig oder abgelaufen'], ], [ 'label' => 'E-Mail verifizieren Invalid', 'path' => 'email-verifizieren.php', 'contains' => ['E-Mail verifizieren', 'ungültig oder abgelaufen'], ], [ 'label' => 'Mandant-Einstellungen Login-Schutz', 'path' => 'mandant-einstellungen.php', 'expect_redirect' => 'login.php', 'contains' => [], ], [ 'label' => 'Namensanpassung', 'path' => 'namenanpassen.php', 'auth' => true, 'contains' => ['Anzeigenamen aktualisieren'], ], [ 'label' => 'Datenexport Login-Schutz', 'path' => 'datenexport.php', 'expect_redirect' => 'login.php', 'contains' => [], ], [ 'label' => 'Abo-Upgrade GET-Redirect', 'path' => 'abo-upgrade.php', 'expect_redirect' => 'login.php', 'contains' => [], ], [ 'label' => 'Abo-Portal GET-Redirect', 'path' => 'abo-portal.php', 'expect_redirect' => 'login.php', 'contains' => [], ], [ 'label' => 'Mandant löschen Login-Schutz', 'path' => 'mandant-loeschen.php', 'expect_redirect' => 'login.php', 'contains' => [], ], [ 'label' => 'Back-Office Login-Schutz', 'path' => 'backoffice.php', 'expect_redirect' => 'login.php', 'contains' => [], ], [ 'label' => 'Back-Office Mandantendetail Login-Schutz', 'path' => 'backoffice-mandant.php?tenant_id=1', 'expect_redirect' => 'login.php', 'contains' => [], ], [ 'label' => 'Teilnehmerauswertung', 'path' => 'PLATZHALTER_TEILNEHMER', 'auth' => true, 'contains' => ['Auswertung', 'Smoke Guthaben', 'Jahresübersicht', 'Letzte Einzahlungen'], ], [ 'label' => 'PDF-Export', 'path' => 'exportKaffeeliste.php', 'auth' => true, 'contains' => ['%PDF'], ], [ 'label' => 'Mailversand', 'path' => 'mailversenden.php', 'auth' => true, 'contains' => ['Info-Mail versenden', 'Dry-Run', 'Versandlog'], ], [ 'label' => 'Jahresabschluss', 'path' => 'jahresauswertung.php', 'auth' => true, 'contains' => ['Jahresabschluss', 'Dry-Run', 'Jahresstriche gesamt'], ], ]; $knownIssueChecks = [ ]; $skippedUnsafe = [ ]; /** * HTTP-Request mit optionalem Cookie-Jar. $cookies wird per Referenz * aktualisiert, damit die Session ueber mehrere Requests erhalten bleibt. * * @param array|null $cookies null = bewusst ohne Session */ function smoke_fetch(string $url, ?array &$cookies = null, string $method = 'GET', ?string $postBody = null): array { $header = "User-Agent: KaffeelisteHttpSmoke/1.0\r\n"; if ($cookies !== null && $cookies !== []) { $pairs = []; foreach ($cookies as $name => $value) { $pairs[] = "{$name}={$value}"; } $header .= 'Cookie: ' . implode('; ', $pairs) . "\r\n"; } if ($postBody !== null) { $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; } $context = stream_context_create([ 'http' => [ 'method' => $method, 'timeout' => 15, 'ignore_errors' => true, 'follow_location' => 0, 'header' => $header, 'content' => $postBody ?? '', ], ]); $body = @file_get_contents($url, false, $context); $headers = $http_response_header ?? []; $status = 0; $location = null; foreach ($headers as $h) { if (preg_match('~^HTTP/\S+\s+(\d{3})~', $h, $matches) === 1) { $status = (int)$matches[1]; } if (preg_match('/^Location:\s*(.+)$/i', $h, $m) === 1) { $location = trim($m[1]); } if ($cookies !== null && preg_match('/^Set-Cookie:\s*([^=;]+)=([^;]+)/i', $h, $m) === 1) { $cookies[$m[1]] = $m[2]; } } if ($body === false) { $error = error_get_last(); return [ 'status' => $status, 'body' => '', 'location' => $location, 'error' => $error['message'] ?? 'Unknown HTTP error', ]; } return [ 'status' => $status, 'body' => $body, 'location' => $location, 'error' => null, ]; } /** * Legt einen eigenen Mandanten mit bekannten Zahlen an: * - "Smoke Guthaben" +20,00 € Einzahlung, 25 Striche a 0,20 € = 15,00 € * - "Smoke Schulden" keine Einzahlung, 20 Striche a 0,20 € = -4,00 € * - "Smoke Ohne Buchungen" nichts = 0,00 € * - "Smoke Inaktiv" inaktiv, darf in der Kaffeeliste fehlen * * @return array{tenant_id:int, participant_id:int} */ function smoke_seed(PDO $pdo, string $slug, string $email, string $password, string $suffix): array { $pdo->prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)') ->execute([$slug, 'HTTP-Smoke Mandant', 'active']); $tenantId = (int)$pdo->lastInsertId(); $pdo->prepare('INSERT INTO tenant_settings (tenant_id) VALUES (?)')->execute([$tenantId]); $pdo->prepare('INSERT INTO users (email, email_norm, display_name, password_hash, status) VALUES (?, ?, ?, ?, ?)') ->execute([$email, $email, 'Smoke Owner', password_hash($password, PASSWORD_DEFAULT), 'active']); $userId = (int)$pdo->lastInsertId(); $pdo->prepare('INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at) VALUES (?, ?, ?, ?, NOW())') ->execute([$tenantId, $userId, 'owner', 'active']); $insertParticipant = $pdo->prepare( 'INSERT INTO participants (tenant_id, user_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, ?, ?)' ); $ids = []; foreach ([['Smoke Guthaben', $userId, 1], ['Smoke Schulden', null, 1], ['Smoke Ohne Buchungen', null, 1], ['Smoke Inaktiv', null, 0]] as [$name, $uid, $active]) { $mail = strtolower(str_replace(' ', '-', $name)) . "-{$suffix}@test.local"; $insertParticipant->execute([$tenantId, $uid, $name, $mail, $mail, $active]); $ids[$name] = (int)$pdo->lastInsertId(); } $payment = $pdo->prepare( "INSERT INTO ledger_entries (tenant_id, participant_id, type, amount_cents, booked_at, source) VALUES (?, ?, 'payment', ?, NOW(), 'manual_bulk')" ); $consumption = $pdo->prepare( "INSERT INTO ledger_entries (tenant_id, participant_id, type, amount_cents, marks_count, unit_price_cents, booked_at, source) VALUES (?, ?, 'consumption', ?, ?, 20, NOW(), 'manual_bulk')" ); $payment->execute([$tenantId, $ids['Smoke Guthaben'], 2000]); $consumption->execute([$tenantId, $ids['Smoke Guthaben'], -500, 25]); $consumption->execute([$tenantId, $ids['Smoke Schulden'], -400, 20]); return ['tenant_id' => $tenantId, 'participant_id' => $ids['Smoke Guthaben']]; } function smoke_cleanup(PDO $pdo, ?int $tenantId, string $email): void { if ($tenantId === null) { return; } $pdo->prepare('DELETE FROM ledger_entries WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM participants WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM tenant_memberships WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM tenant_settings WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM users WHERE email_norm = ?')->execute([strtolower($email)]); } function smoke_url(string $baseUrl, string $path): string { return $baseUrl . '/' . ltrim($path, '/'); } $failures = []; $passes = 0; echo "HTTP smoke base URL: {$baseUrl}\n"; // Testmandant anlegen und per HTTP anmelden. try { $seeded = smoke_seed($pdo, $smokeSlug, $smokeEmail, $smokePassword, $suffix); $smokeTenantId = $seeded['tenant_id']; } catch (Throwable $e) { fwrite(STDERR, "HTTP smoke setup failed: {$e->getMessage()}\n"); exit(1); } // Pfad der Teilnehmerauswertung steht erst nach dem Seeden fest. foreach ($checks as $i => $check) { if ($check['path'] === 'PLATZHALTER_TEILNEHMER') { $checks[$i]['path'] = 'teilnehmerauswertung.php?participant_id=' . $seeded['participant_id']; } } $authCookies = []; $loginPage = smoke_fetch(smoke_url($baseUrl, 'login.php'), $authCookies); preg_match('/name="csrf_token" value="([^"]+)"/', $loginPage['body'], $csrfMatch); smoke_fetch(smoke_url($baseUrl, 'login.php'), $authCookies, 'POST', http_build_query([ 'csrf_token' => $csrfMatch[1] ?? '', 'email' => $smokeEmail, 'password' => $smokePassword, 'tenant_slug' => $smokeSlug, ])); if (empty($authCookies)) { smoke_cleanup($pdo, $smokeTenantId, $smokeEmail); fwrite(STDERR, "HTTP smoke: Anmeldung fehlgeschlagen (keine Session).\n"); exit(1); } foreach ($checks as $check) { $url = smoke_url($baseUrl, $check['path']); // Angemeldete Seiten mit Session, alles andere bewusst ohne - nur so // laesst sich der Login-Schutz ueberhaupt pruefen. $needsAuth = !empty($check['auth']); $jar = $needsAuth ? $authCookies : null; $response = smoke_fetch($url, $jar); $label = $check['label'] . ' (' . $check['path'] . ')'; if ($response['error'] !== null) { $failures[] = "{$label}: {$response['error']}"; echo "FAIL {$label}\n"; continue; } // Geschuetzte Seiten muessen ohne Session auf den Login umleiten. Das // explizit zu pruefen ist belastbarer, als der Umleitung zu folgen und im // Ergebnis nach dem Wort "Login" zu suchen. if (isset($check['expect_redirect'])) { $ziel = (string)$response['location']; if ($response['status'] !== 302 || !str_contains($ziel, $check['expect_redirect'])) { $failures[] = "{$label}: erwartete Umleitung auf {$check['expect_redirect']}, kam HTTP {$response['status']} nach '{$ziel}'"; echo "FAIL {$label}\n"; continue; } $passes++; echo "PASS {$label}\n"; continue; } if ($response['status'] !== 200) { $failures[] = "{$label}: expected HTTP 200, got HTTP {$response['status']}"; echo "FAIL {$label}\n"; continue; } if (preg_match($issuePattern, $response['body'], $matches) === 1) { $failures[] = "{$label}: PHP issue marker found ({$matches[0]})"; echo "FAIL {$label}\n"; continue; } foreach ($check['contains'] as $needle) { if (!str_contains($response['body'], $needle)) { $failures[] = "{$label}: expected text not found: {$needle}"; echo "FAIL {$label}\n"; continue 2; } } foreach (($check['not_contains'] ?? []) as $needle) { if (str_contains($response['body'], $needle)) { $failures[] = "{$label}: unexpected text found: {$needle}"; echo "FAIL {$label}\n"; continue 2; } } $passes++; echo "PASS {$label}\n"; } foreach ($knownIssueChecks as $check) { $url = smoke_url($baseUrl, $check['path']); $response = smoke_fetch($url); $label = $check['label'] . ' (' . $check['path'] . ')'; if ($response['error'] !== null) { echo "KNOWN {$label}: {$check['reason']} ({$response['error']})\n"; continue; } if (preg_match($check['expected_issue'], $response['body']) === 1) { echo "KNOWN {$label}: {$check['reason']}\n"; continue; } if ($response['status'] === 200 && preg_match($issuePattern, $response['body']) !== 1) { echo "RESOLVED {$label}: bitte in die regulären Smoke-Checks aufnehmen.\n"; continue; } $failures[] = "{$label}: unexpected response while checking known issue"; echo "FAIL {$label}\n"; } foreach ($skippedUnsafe as $skip) { echo "SKIP {$skip['path']}: {$skip['reason']}\n"; } smoke_cleanup($pdo, $smokeTenantId, $smokeEmail); if ($failures !== []) { echo "\nHTTP smoke failed with " . count($failures) . " failure(s):\n"; foreach ($failures as $failure) { echo "- {$failure}\n"; } exit(1); } echo "\nHTTP smoke passed with {$passes} checked pages.\n";