263 lines
9.9 KiB
JavaScript
263 lines
9.9 KiB
JavaScript
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}/${timestamp}-eiamselect.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}/${timestamp}-userid.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}/${timestamp}-pw.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}/${timestamp}totp.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}/${timestamp}-result.png`, fullPage: true });
|
||
console.log(` Suche nach Benutzername: ${SUCCESS_TEXT}`);
|
||
const pageText = await page.textContent('body');
|
||
fs.writeFileSync(path.join(resultDir, `${timestamp}-final.html`), await page.content());
|
||
fs.writeFileSync(path.join(resultDir, `${timestamp}-final.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}/${timestamp}logout.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, `${timestamp}-error.png`);
|
||
const htmlPath = path.join(resultDir, `${timestamp}-page.html`);
|
||
const textPath = path.join(resultDir, `${timestamp}-page.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();
|
||
}
|
||
})(); |