const { chromium } = require('playwright'); const fs = require('fs'); const path = require('path'); const dotenv = require('dotenv'); const speakeasy = require('speakeasy'); const { URL } = require('url'); const http = require('http'); const https = require('https'); const nodemailer = require('nodemailer'); // Lade .env-Variablen dotenv.config(); const { USERNAME, PASSWORD, TESTNAME, TOTP_SECRET, LOGIN_URL = 'about:privatebrowsing', LOGOUT_URL = 'about:privatebrowsing', SUCCESS_TEXT = "", SMTP_HOST, SMTP_PORT, MAIL_FROM, MAIL_TO, PUSHGATEWAY_URL } = process.env; // E-Mail bei Fehlschlag senden async function sendFailureMail(subject, text, attachments) { const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, port: parseInt(process.env.SMTP_PORT, 10), secure: false, // kein TLS tls: { rejectUnauthorized: false } }); await transporter.sendMail({ from: process.env.MAIL_FROM, to: process.env.MAIL_TO, subject, text, attachments }); } // Prüfe notwendige ENV-Variablen if (!USERNAME || !PASSWORD || !TOTP_SECRET) { console.error('❌ Bitte .env mit USERNAME, PASSWORD und TOTP_SECRET konfigurieren'); process.exit(2); } if (!PUSHGATEWAY_URL) { console.error('❌ PUSHGATEWAY_URL nicht gesetzt – Schritte werden nicht an PushGateway gesendet'); } (async () => { const resultDir = '/app/test-results'; const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const browser = await chromium.launch({ headless: true }); const context = await browser.newContext({}); const page = await context.newPage(); const stepsData = []; const overallStartTime = Date.now(); // Hilfsfunktion: Sende Metriktext an PushGateway (falls URL konfiguriert) async function sendMetrics(lines) { if (!PUSHGATEWAY_URL) return; const pushUrl = new URL(PUSHGATEWAY_URL); const protocolLib = pushUrl.protocol === 'https:' ? https : http; const options = { hostname: pushUrl.hostname, path: `/metrics/job/${encodeURIComponent(TESTNAME)}`, method: 'POST', headers: { 'Content-Type': 'text/plain' } }; if (pushUrl.port) { options.port = pushUrl.port; } await new Promise((resolve, reject) => { const req = protocolLib.request(options, res => { res.on('error', reject); res.on('data', () => {}); // Antwortinhalt ignorieren res.on('end', () => { if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) { resolve(); } else { reject(new Error(`PushGateway HTTP-Status: ${res.statusCode} ${PUSHGATEWAY_URL} ${lines}`)); } }); }); req.on('error', reject); req.write(lines.join('\n') + '\n'); req.end(); }); } // Hilfsfunktion: Führe einen Schritt aus und erfasse Metriken async function doStep(stepNumber, stepName, stepFunction) { const stepStart = Date.now(); let success = true; let errorMessage = ""; try { await stepFunction(); } catch (err) { success = false; errorMessage = err.message; } const stepEnd = Date.now(); const duration = stepEnd - stepStart; const timeSinceStart = stepEnd - overallStartTime; const stepMetrics = { stepNumber, stepName, success, errorMessage, duration, timeSinceStart }; // Ausgabe des Schritts als JSON-Log stepsData.push(stepMetrics); console.log(JSON.stringify(stepMetrics)); // Vorbereitung des Metrik-Eintrags const safe = (val) => ("" + val).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n'); const baseLabels = `step="${stepNumber}", step_name="${safe(stepName)}"${TESTNAME ? `, test="${safe(TESTNAME)}"` : ""}`; const metricLines = []; // Erfolg (Gauge 0/1, Fehlertext als Label falls vorhanden) if (errorMessage) { metricLines.push(`monitor_step_success{${baseLabels}, error="${safe(errorMessage)}"} ${success ? 1 : 0}`); } else { metricLines.push(`monitor_step_success{${baseLabels}} ${success ? 1 : 0}`); } // Dauer und Zeit seit Start (ms) metricLines.push(`monitor_step_duration_ms{${baseLabels}} ${duration}`); metricLines.push(`monitor_step_elapsed_ms{${baseLabels}} ${timeSinceStart}`); try { await sendMetrics(metricLines); } catch (pushErr) { console.error(`❌ Fehler beim Senden der Metriken für Schritt ${stepNumber}:`, pushErr.message); } if (!success) { // Bei Fehler Abbruch auslösen throw new Error(errorMessage || `Step ${stepNumber} failed`); } } try { // Schritt 1: Login-Seite aufrufen und zum eIAM-Login gelangen console.log(`${TESTNAME}: 1 Login aufrufen`); await doStep(1, 'Login aufrufen', async () => { await page.goto(LOGIN_URL); await page.setViewportSize({ width: 1920, height: 937 }); await Promise.all([ page.click('span:nth-child(2)'), page.waitForNavigation() ]); await page.screenshot({ path: `${resultDir}/eiamselect-${timestamp}.png`, fullPage: true }); await Promise.all([ page.click('#social-eIAM'), page.waitForNavigation() ]); }); // Schritt 2: Benutzername eingeben console.log('✅ Login eIAM Benutzername eintragen'); await doStep(2, 'Benutzernamen eingeben', async () => { await page.click('#isiwebuserid'); await page.fill('#isiwebuserid', USERNAME); await page.screenshot({ path: `${resultDir}/userid-${timestamp}.png`, fullPage: true }); await Promise.all([ page.press('#isiwebuserid', 'Enter'), page.waitForNavigation() ]); }); // Schritt 3: Passwort eingeben console.log('✅ Login eIAM Passwort eintragen'); await doStep(3, 'Passwort eingeben', async () => { await page.click('#isiwebpasswd'); await page.fill('#isiwebpasswd', PASSWORD); await page.screenshot({ path: `${resultDir}/pw-${timestamp}.png`, fullPage: true }); await Promise.all([ page.press('#isiwebpasswd', 'Enter'), page.waitForNavigation() ]); }); // Schritt 4: TOTP eingeben console.log('✅ Login eIAM TOTP generieren'); await doStep(4, 'TOTP eingeben', async () => { const totp = speakeasy.totp({ secret: TOTP_SECRET, encoding: 'base32' }); console.log(`✅ Login eIAM TOTP Code: ${totp}`); console.log('✅ Login eIAM TOTP eintragen'); await page.fill('#code2FA', totp); await page.screenshot({ path: `${resultDir}/totp-${timestamp}.png`, fullPage: true }); await Promise.all([ page.press('#code2FA', 'Enter'), page.waitForNavigation() ]); }); // Schritt 5: Login-Erfolg prüfen (Benutzername sichtbar?) console.log('✅ WhoAmI Seite laden und prüfen'); await doStep(5, 'Login-Erfolg prüfen', async () => { await page.waitForSelector(`text=${SUCCESS_TEXT}`, { timeout: 10000 }); await page.screenshot({ path: `${resultDir}/result-${timestamp}.png`, fullPage: true }); console.log(` Suche nach Benutzername: ${SUCCESS_TEXT}`); const pageText = await page.textContent('body'); fs.writeFileSync(path.join(resultDir, `final-${timestamp}.html`), await page.content()); fs.writeFileSync(path.join(resultDir, `final-${timestamp}.txt`), pageText); if (!pageText.includes(SUCCESS_TEXT)) { throw new Error('Benutzername nicht gefunden – Login möglicherweise fehlerhaft'); } console.log('✅ Login erfolgreich – Benutzername sichtbar'); }); // Schritt 6: Logout durchführen console.log('✅ Logout'); await doStep(6, 'Logout', async () => { await page.goto(LOGOUT_URL); await Promise.all([ page.click('#kc-logout'), page.waitForNavigation() ]); await page.screenshot({ path: `${resultDir}/logout-${timestamp}.png`, fullPage: true }); console.log('✅ Logout erfolgreich'); }); console.log('✅ Alle Schritte erfolgreich abgeschlossen'); // Schreibe alle Schritt-Metriken in die Datei fs.writeFileSync(path.join(resultDir, 'step-metrics.json'), JSON.stringify(stepsData, null, 2)); } catch (err) { console.error('❌ Fehler beim Login:', err.message); // Fehlerbehandlung und Ergebnisse sichern const screenshotPath = path.join(resultDir, `error_${timestamp}.png`); const htmlPath = path.join(resultDir, `page_${timestamp}.html`); const textPath = path.join(resultDir, `page-${timestamp}.txt`); await page.screenshot({ path: screenshotPath }); const pageText = await page.textContent('body'); fs.writeFileSync(htmlPath, await page.content()); fs.writeFileSync(textPath, pageText); const captchaDetected = await page.evaluate(() => { return document.body.innerText.toLowerCase().includes('captcha') || document.querySelector('input[name="captcha"]') !== null || document.querySelector('.g-recaptcha') !== null; }); await sendFailureMail( '❌ OIDC Login Monitoring fehlgeschlagen', `Fehler beim Login:\n\n${err.message}\n\nZeitpunkt: ${timestamp}`, [ { filename: 'error.png', path: screenshotPath }, { filename: 'page.html', path: htmlPath }, { filename: 'page.txt', path: textPath } ] ); // Schritt-Metriken (bis zum Fehler) in Datei schreiben fs.writeFileSync(path.join(resultDir, 'step-metrics.json'), JSON.stringify(stepsData, null, 2)); if (captchaDetected) { console.error('❌ Captcha erkannt – Test wird abgebrochen'); const captchaPath = path.join(resultDir, `captcha_${timestamp}.png`); const captchaHtmlPath = path.join(resultDir, `captcha_${timestamp}.html`); const captchaTextPath = path.join(resultDir, `captcha_${timestamp}.txt`); await page.screenshot({ path: captchaPath }); fs.writeFileSync(captchaHtmlPath, await page.content()); fs.writeFileSync(captchaTextPath, await page.evaluate(() => document.body.innerText)); process.exit(3); // spezieller Exit-Code für Captcha } process.exit(1); } finally { await browser.close(); } })();