$cookies * @return array{status:int, body:string, location:?string} */ function konto_request(string $url, array &$cookies, string $method = 'GET', ?string $postBody = null): array { $cookieHeader = ''; if ($cookies !== []) { $pairs = []; foreach ($cookies as $name => $value) { $pairs[] = "{$name}={$value}"; } $cookieHeader = 'Cookie: ' . implode('; ', $pairs) . "\r\n"; } $headers = "User-Agent: KaffeelisteKontoCheck/1.0\r\n" . $cookieHeader; if ($postBody !== null) { $headers .= "Content-Type: application/x-www-form-urlencoded\r\n"; } $context = stream_context_create([ 'http' => [ 'method' => $method, 'header' => $headers, 'content' => $postBody, 'timeout' => 15, 'ignore_errors' => true, 'follow_location' => 0, ], ]); $body = @file_get_contents($url, false, $context); $responseHeaders = $http_response_header ?? []; $status = 0; $location = null; foreach ($responseHeaders as $header) { if (preg_match('~^HTTP/\S+\s+(\d{3})~', $header, $m) === 1) { $status = (int)$m[1]; } if (preg_match('/^Set-Cookie:\s*([^=;]+)=([^;]+)/i', $header, $m) === 1) { $cookies[$m[1]] = $m[2]; } if (preg_match('/^Location:\s*(.+)$/i', $header, $m) === 1) { $location = trim($m[1]); } } return ['status' => $status, 'body' => (string)$body, 'location' => $location]; } /** @param array $cookies */ function konto_csrf(string $url, array &$cookies): string { $page = konto_request($url, $cookies); preg_match('/name="csrf_token" value="([^"]+)"/', $page['body'], $m); return $m[1] ?? ''; } $failures = []; $passes = 0; function pruefe(string $label, bool $ok): void { global $failures, $passes; if ($ok) { $passes++; echo "PASS {$label}\n"; } else { $failures[] = $label; echo "FAIL {$label}\n"; } } $tenantAId = null; $tenantBId = null; $tenantCId = null; $userId = null; $participantId = null; try { // Zwei Mandanten fuer den Wechsel plus ein dritter, in dem der Testnutzer // ausdruecklich kein Mitglied ist (Negativfall). foreach (['a', 'b', 'c'] as $key) { $stmt = $pdo->prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)'); $stmt->execute(["kontocheck-{$key}-{$suffix}", "Kontocheck {$key}", 'active']); $id = (int)$pdo->lastInsertId(); $pdo->prepare('INSERT INTO tenant_settings (tenant_id) VALUES (?)')->execute([$id]); ${'tenant' . strtoupper($key) . 'Id'} = $id; } $email = "kontocheck-{$suffix}@test.local"; $pdo->prepare( 'INSERT INTO users (email, email_norm, display_name, password_hash, status) VALUES (?, ?, ?, ?, ?)' )->execute([$email, $email, 'Kontocheck Nutzer', password_hash($altesPasswort, PASSWORD_DEFAULT), 'active']); $userId = (int)$pdo->lastInsertId(); foreach ([$tenantAId, $tenantBId] as $tid) { $pdo->prepare( 'INSERT INTO tenant_memberships (tenant_id, user_id, role, status, joined_at) VALUES (?, ?, ?, ?, NOW())' )->execute([$tid, $userId, 'owner', 'active']); } // Teilnehmer in Mandant A, dem die Einladung gelten soll. $einladungsMail = "kontocheck-invite-{$suffix}@test.local"; $pdo->prepare( 'INSERT INTO participants (tenant_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, 1)' )->execute([$tenantAId, 'Einzuladendes Mitglied', $einladungsMail, $einladungsMail]); $participantId = (int)$pdo->lastInsertId(); } catch (Throwable $e) { fwrite(STDERR, "Kontocheck-Setup fehlgeschlagen: {$e->getMessage()}\n"); exit(1); } try { // --------------------------------------------------------------- // 4. Login landet auf dem Dashboard // --------------------------------------------------------------- $cookies = []; $csrf = konto_csrf("{$baseUrl}/login.php", $cookies); $login = konto_request("{$baseUrl}/login.php", $cookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'email' => "kontocheck-{$suffix}@test.local", 'password' => $altesPasswort, 'tenant_slug' => "kontocheck-a-{$suffix}", ])); pruefe('Login leitet auf index.php statt konto.php', $login['location'] === 'index.php'); // --------------------------------------------------------------- // 3. Verifikation wird fuer Einladungen erzwungen // --------------------------------------------------------------- $csrf = konto_csrf("{$baseUrl}/mitarbeiterverwalten.php", $cookies); $einladung = konto_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'aktion' => 'zugang_gewaehren', 'mitgliedID' => (string)$participantId, 'rolle' => 'member', ])); pruefe( 'Einladung ohne bestaetigte Adresse wird abgelehnt', str_contains($einladung['body'], 'bestätigt sein') ); $stmt = $pdo->prepare('SELECT user_id FROM participants WHERE id = ?'); $stmt->execute([$participantId]); pruefe('Ohne Verifikation wurde kein Konto angelegt', (int)($stmt->fetchColumn() ?: 0) === 0); // Adresse bestaetigen und dieselbe Aktion erneut versuchen. $pdo->prepare('UPDATE users SET email_verified_at = NOW() WHERE id = ?')->execute([$userId]); $csrf = konto_csrf("{$baseUrl}/mitarbeiterverwalten.php", $cookies); $einladung = konto_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'aktion' => 'zugang_gewaehren', 'mitgliedID' => (string)$participantId, 'rolle' => 'member', ])); pruefe( 'Einladung mit bestaetigter Adresse funktioniert', str_contains($einladung['body'], 'Zugang wurde gewährt') ); // --------------------------------------------------------------- // 2. Mandantenwechsel aus bestehender Sitzung // --------------------------------------------------------------- $auswahl = konto_request("{$baseUrl}/mandant-auswahl.php", $cookies); pruefe( 'Mandantenwechsel ist eingeloggt erreichbar (kein Redirect auf login.php)', $auswahl['status'] === 200 && str_contains($auswahl['body'], 'Mandant wechseln') ); $csrf = konto_csrf("{$baseUrl}/mandant-auswahl.php", $cookies); $wechsel = konto_request("{$baseUrl}/mandant-auswahl.php", $cookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'tenant_id' => (string)$tenantBId, ])); pruefe('Wechsel auf eigenen zweiten Mandanten leitet aufs Dashboard', $wechsel['location'] === 'index.php'); $konto = konto_request("{$baseUrl}/konto.php", $cookies); pruefe( 'Nach dem Wechsel ist Mandant B aktiv', str_contains($konto['body'], "kontocheck-b-{$suffix}") ); $csrf = konto_csrf("{$baseUrl}/mandant-auswahl.php", $cookies); $fremd = konto_request("{$baseUrl}/mandant-auswahl.php", $cookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'tenant_id' => (string)$tenantCId, ])); pruefe( 'Wechsel auf fremden Mandanten wird abgelehnt', $fremd['location'] === null && str_contains($fremd['body'], 'nicht verfügbar') ); $konto = konto_request("{$baseUrl}/konto.php", $cookies); pruefe( 'Nach abgelehntem Wechsel ist weiterhin Mandant B aktiv', str_contains($konto['body'], "kontocheck-b-{$suffix}") ); // --------------------------------------------------------------- // 1. Passwortwechsel // --------------------------------------------------------------- $falsch = saas_change_password($pdo, $userId, 'VoelligFalsch999!', $neuesPasswort, $neuesPasswort); pruefe('Falsches aktuelles Passwort wird abgelehnt', $falsch['ok'] === false); $ungleich = saas_change_password($pdo, $userId, $altesPasswort, $neuesPasswort, 'tippfehler'); pruefe('Abweichende Wiederholung wird abgelehnt', $ungleich['ok'] === false); $zuKurz = saas_change_password($pdo, $userId, $altesPasswort, 'kurz', 'kurz'); pruefe('Zu kurzes Passwort wird abgelehnt', $zuKurz['ok'] === false); $gleich = saas_change_password($pdo, $userId, $altesPasswort, $altesPasswort, $altesPasswort); pruefe('Unveraendertes Passwort wird abgelehnt', $gleich['ok'] === false); $stmt = $pdo->prepare('SELECT password_hash FROM users WHERE id = ?'); $stmt->execute([$userId]); pruefe( 'Nach allen Fehlversuchen gilt weiterhin das alte Passwort', password_verify($altesPasswort, (string)$stmt->fetchColumn()) ); $ok = saas_change_password($pdo, $userId, $altesPasswort, $neuesPasswort, $neuesPasswort); pruefe('Korrekter Passwortwechsel wird gespeichert', $ok['ok'] === true); // Der Wechsel muss sich auch am echten Login zeigen. $neueCookies = []; $csrf = konto_csrf("{$baseUrl}/login.php", $neueCookies); $altLogin = konto_request("{$baseUrl}/login.php", $neueCookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'email' => "kontocheck-{$suffix}@test.local", 'password' => $altesPasswort, 'tenant_slug' => "kontocheck-a-{$suffix}", ])); pruefe('Login mit altem Passwort schlaegt fehl', $altLogin['location'] === null); $neueCookies = []; $csrf = konto_csrf("{$baseUrl}/login.php", $neueCookies); $neuLogin = konto_request("{$baseUrl}/login.php", $neueCookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'email' => "kontocheck-{$suffix}@test.local", 'password' => $neuesPasswort, 'tenant_slug' => "kontocheck-a-{$suffix}", ])); pruefe('Login mit neuem Passwort funktioniert', $neuLogin['location'] === 'index.php'); // --------------------------------------------------------------- // Reset-Link bestaetigt die Adresse mit // --------------------------------------------------------------- $pdo->prepare('UPDATE users SET email_verified_at = NULL WHERE id = ?')->execute([$userId]); $token = saas_create_auth_token($pdo, $userId, 'password_reset', $tenantAId, 60); $reset = saas_reset_password_with_token($pdo, $token['token'], 'ResetCheck789!', 'ResetCheck789!'); $stmt = $pdo->prepare('SELECT email_verified_at FROM users WHERE id = ?'); $stmt->execute([$userId]); pruefe( 'Passwort-Reset per Mail-Link bestaetigt die Adresse mit', $reset['ok'] === true && $stmt->fetchColumn() !== null ); } finally { // Aufraeumen: Mitgliedschaften/Teilnehmer haengen per Fremdschluessel an // den Mandanten, der Nutzer wird separat entfernt. foreach ([$tenantAId, $tenantBId, $tenantCId] as $tid) { if ($tid !== null) { $pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tid]); } } if ($userId !== null) { $pdo->prepare('DELETE FROM user_auth_tokens WHERE user_id = ?')->execute([$userId]); $pdo->prepare('DELETE FROM users WHERE id = ?')->execute([$userId]); } // Das eingeladene Mitglied bekam bei Erfolg ein eigenes Benutzerkonto. $pdo->prepare('DELETE FROM users WHERE email_norm = ?') ->execute(["kontocheck-invite-{$suffix}@test.local"]); } if ($failures !== []) { fwrite(STDERR, "\nKontocheck fehlgeschlagen:\n - " . implode("\n - ", $failures) . "\n"); exit(1); } echo "\nKonto- und Mandantenwechsel-Check bestanden mit {$passes} Assertions.\n";