333 lines
13 KiB
PHP
333 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
if (PHP_SAPI !== 'cli') {
|
|
http_response_code(403);
|
|
exit("Dieses Skript ist nur fuer die Kommandozeile gedacht.\n");
|
|
}
|
|
|
|
/**
|
|
* Prueft die vier Kontofunktionen, die vor dem Go-Live ergaenzt wurden:
|
|
*
|
|
* 1. Passwortwechsel im eingeloggten Zustand (saas_change_password).
|
|
* 2. Mandantenwechsel aus einer bestehenden Sitzung heraus.
|
|
* 3. Erzwungene E-Mail-Verifikation fuer Aktionen, die nach aussen mailen.
|
|
* 4. Login landet auf dem Dashboard statt auf der Kontoseite.
|
|
*
|
|
* Braucht einen laufenden Dev-Server (scripts/run-dev-server.sh).
|
|
* Legt eigene Testmandanten an und raeumt sie am Ende wieder ab.
|
|
*/
|
|
|
|
require __DIR__ . '/dev-db.php';
|
|
require_once __DIR__ . '/../app/saas-auth.php';
|
|
|
|
$baseUrl = rtrim((string)(getenv('SMOKE_BASE_URL') ?: 'http://127.0.0.1:8080'), '/');
|
|
$pdo = dev_pdo();
|
|
$suffix = bin2hex(random_bytes(4));
|
|
$altesPasswort = 'KontoCheckAlt123!';
|
|
$neuesPasswort = 'KontoCheckNeu456!';
|
|
|
|
/**
|
|
* Minimaler Cookie-Jar auf Basis von file_get_contents, weil diese
|
|
* PHP-Installation keine curl-Extension hat - gleiches Vorgehen wie in
|
|
* scripts/check-m8-role-matrix.php.
|
|
*
|
|
* @param array<string,string> $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];
|
|
}
|
|
|
|
function konto_follow_redirect(array $response, array &$cookies, string $baseUrl): array
|
|
{
|
|
if ($response['status'] < 300 || $response['status'] >= 400 || $response['location'] === null) {
|
|
return $response;
|
|
}
|
|
|
|
$location = (string)$response['location'];
|
|
if (preg_match('~^https?://~i', $location) === 1) {
|
|
$url = $location;
|
|
} else {
|
|
$url = $baseUrl . '/' . ltrim($location, '/');
|
|
}
|
|
|
|
return konto_request($url, $cookies);
|
|
}
|
|
|
|
/** @param array<string,string> $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_follow_redirect(konto_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
|
|
'csrf_token' => $csrf,
|
|
'aktion' => 'zugang_gewaehren',
|
|
'mitgliedID' => (string)$participantId,
|
|
'rolle' => 'member',
|
|
])), $cookies, $baseUrl);
|
|
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_follow_redirect(konto_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([
|
|
'csrf_token' => $csrf,
|
|
'aktion' => 'zugang_gewaehren',
|
|
'mitgliedID' => (string)$participantId,
|
|
'rolle' => 'member',
|
|
])), $cookies, $baseUrl);
|
|
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";
|