diff --git a/test/monitor.js b/test/monitor.js new file mode 100644 index 0000000..f0ef1da --- /dev/null +++ b/test/monitor.js @@ -0,0 +1,259 @@ +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 = "", + 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: pushUrl.pathname + pushUrl.search, + 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}`)); + } + }); + }); + 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(); + } +})(); \ No newline at end of file