$cookies * @return array{status:int, body:string} */ function bc_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: KaffeelisteBillingCap/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; 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]; } } return ['status' => $status, 'body' => (string)$body]; } function bc_csrf(string $body): string { preg_match('/name="csrf_token" value="([^"]+)"/', $body, $matches); return $matches[1] ?? ''; } $tenantId = null; $userId = null; $extraParticipantIds = []; try { $stmt = $pdo->prepare('INSERT INTO tenants (slug, name, status) VALUES (?, ?, ?)'); $stmt->execute(["billingcap-{$suffix}", 'Billing Cap Tenant', 'active']); $tenantId = (int)$pdo->lastInsertId(); $pdo->prepare('INSERT INTO tenant_settings (tenant_id) VALUES (?)')->execute([$tenantId]); $email = "billingcap-owner-{$suffix}@test.local"; $pdo->prepare('INSERT INTO users (email, email_norm, display_name, password_hash, status) VALUES (?, ?, ?, ?, ?)') ->execute([$email, $email, 'Billing Cap 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']); $pdo->prepare('INSERT INTO participants (tenant_id, user_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, ?, 1)') ->execute([$tenantId, $userId, 'Billing Cap Owner', $email, $email]); // Free-Plan-Limit ist 10. Owner zaehlt schon als 1 aktiver Teilnehmer, // also 8 weitere anlegen, um bei 9 von 10 zu landen. for ($i = 1; $i <= 8; $i++) { $pEmail = "billingcap-seed{$i}-{$suffix}@test.local"; $pdo->prepare('INSERT INTO participants (tenant_id, display_name, email, email_norm, active) VALUES (?, ?, ?, ?, 1)') ->execute([$tenantId, "Seed {$i}", $pEmail, $pEmail]); $extraParticipantIds[] = (int)$pdo->lastInsertId(); } } catch (Throwable $e) { fwrite(STDERR, "Billing capacity test setup failed: {$e->getMessage()}\n"); exit(1); } $failures = []; $passes = 0; function bc_assert(string $label, bool $condition, array &$failures, int &$passes): void { if ($condition) { $passes++; echo "PASS {$label}\n"; } else { $failures[] = $label; echo "FAIL {$label}\n"; } } $cookies = []; $loginPage = bc_request("{$baseUrl}/login.php", $cookies); $csrf = bc_csrf($loginPage['body']); bc_request("{$baseUrl}/login.php", $cookies, 'POST', http_build_query([ 'csrf_token' => $csrf, 'email' => $email, 'password' => $password, 'tenant_slug' => "billingcap-{$suffix}", ])); // 9 aktive Teilnehmer vorhanden (Cap 10) -> ein aktives Anlegen muss klappen (10/10). $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); $body10 = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'anlegen', 'csrf_token' => bc_csrf($page['body']), 'name' => 'Zehntes Mitglied', 'email' => "billingcap-tenth-{$suffix}@test.local", 'paypalname' => '', 'aktiv' => '1', ])); bc_assert('10. aktives Mitglied bei Cap 10 wird angelegt', str_contains($body10['body'], 'wurde angelegt'), $failures, $passes); // Jetzt bei 10/10 -> ein weiteres aktives Anlegen muss abgelehnt werden. $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); $body11 = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'anlegen', 'csrf_token' => bc_csrf($page['body']), 'name' => 'Elftes Mitglied', 'email' => "billingcap-eleventh-{$suffix}@test.local", 'paypalname' => '', 'aktiv' => '1', ])); bc_assert('11. aktives Mitglied bei Cap 10 wird abgelehnt', str_contains($body11['body'], 'maximal 10 aktive Mitglieder'), $failures, $passes); bc_assert('11. Mitglied wurde nicht in der DB angelegt', (int)$pdo->query("SELECT COUNT(*) FROM participants WHERE tenant_id = {$tenantId} AND email = 'billingcap-eleventh-{$suffix}@test.local'")->fetchColumn() === 0, $failures, $passes); // Inaktives Anlegen bei vollem Cap muss weiterhin moeglich sein. $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); $bodyInactive = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'anlegen', 'csrf_token' => bc_csrf($page['body']), 'name' => 'Inaktives Mitglied', 'email' => "billingcap-inactive-{$suffix}@test.local", 'paypalname' => '', ])); bc_assert('Inaktives Mitglied bei vollem Cap wird angelegt', str_contains($bodyInactive['body'], 'wurde angelegt'), $failures, $passes); $inactiveId = (int)$pdo->query("SELECT id FROM participants WHERE tenant_id = {$tenantId} AND email = 'billingcap-inactive-{$suffix}@test.local'")->fetchColumn(); bc_assert('Neu angelegtes Mitglied ist tatsaechlich inaktiv', (int)$pdo->query("SELECT active FROM participants WHERE id = {$inactiveId}")->fetchColumn() === 0, $failures, $passes); // Aktivieren des inaktiven Mitglieds bei vollem Cap muss abgelehnt werden. $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); $bodyActivateBlocked = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'aktivieren', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$inactiveId, ])); bc_assert('Aktivieren bei vollem Cap wird abgelehnt', str_contains($bodyActivateBlocked['body'], 'maximal 10 aktive Mitglieder'), $failures, $passes); bc_assert('Mitglied ist nach abgelehntem Aktivieren weiterhin inaktiv', (int)$pdo->query("SELECT active FROM participants WHERE id = {$inactiveId}")->fetchColumn() === 0, $failures, $passes); // Ein aktives Mitglied deaktivieren (Platz schaffen), dann klappt Aktivieren. $freeUpId = $extraParticipantIds[0]; $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'deaktivieren', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$freeUpId, ])); $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); $bodyActivateOk = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'aktivieren', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$inactiveId, ])); bc_assert('Aktivieren nach Platzschaffen (Deaktivierung) klappt', str_contains($bodyActivateOk['body'], 'wurde aktiviert'), $failures, $passes); // bearbeitenspeichern: ein weiteres inaktives Mitglied anlegen, dann per // Bearbeiten-Formular aktivieren versuchen, wieder bei vollem Cap (10/10). $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'anlegen', 'csrf_token' => bc_csrf($page['body']), 'name' => 'Editier-Testmitglied', 'email' => "billingcap-edit-{$suffix}@test.local", 'paypalname' => '', ])); $editId = (int)$pdo->query("SELECT id FROM participants WHERE tenant_id = {$tenantId} AND email = 'billingcap-edit-{$suffix}@test.local'")->fetchColumn(); $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); $bodyEditBlocked = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'bearbeitenspeichern', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$editId, 'name' => 'Editier-Testmitglied', 'email' => "billingcap-edit-{$suffix}@test.local", 'paypalname' => '', 'aktiv' => '1', ])); bc_assert('bearbeitenspeichern-Reaktivierung bei vollem Cap wird abgelehnt', str_contains($bodyEditBlocked['body'], 'maximal 10 aktive Mitglieder'), $failures, $passes); // Editieren ohne Statuswechsel (bleibt inaktiv) muss weiterhin klappen. $page = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies); $bodyEditOk = bc_request("{$baseUrl}/mitarbeiterverwalten.php", $cookies, 'POST', http_build_query([ 'aktion' => 'bearbeitenspeichern', 'csrf_token' => bc_csrf($page['body']), 'mitgliedID' => (string)$editId, 'name' => 'Editier-Testmitglied Neu', 'email' => "billingcap-edit-{$suffix}@test.local", 'paypalname' => '', ])); bc_assert('bearbeitenspeichern ohne Aktivierung bei vollem Cap klappt', str_contains($bodyEditOk['body'], 'wurde gespeichert'), $failures, $passes); // Aufraeumen $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_billing WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM user_auth_tokens WHERE user_id = ?')->execute([$userId]); $pdo->prepare('DELETE FROM users WHERE id = ?')->execute([$userId]); $pdo->prepare('DELETE FROM tenant_settings WHERE tenant_id = ?')->execute([$tenantId]); $pdo->prepare('DELETE FROM tenants WHERE id = ?')->execute([$tenantId]); if ($failures !== []) { echo "\nBilling capacity check failed with " . count($failures) . " failure(s):\n"; foreach ($failures as $failure) { echo "- {$failure}\n"; } exit(1); } echo "\nBilling capacity check passed with {$passes} assertions.\n";