Files
task-manager/tests/e2e_2fa_german.spec.ts
T
paul 9819d8db0b
continuous-integration/drone/push Build is failing
feat: Notifications page, Sidebar layout fixes, and Achievements enhancements
- implemented /notifications page with overdue/upcoming alerts
- Fixed sidebar scrolling in collapsed mode
- Moved notification button to sidebar footer
- Enhanced Achievements page with streak stats and tooltips
- Improved XP history to show task titles
- Added missing translations (en/de)
- Removed top bar header
- Fixed Docker environment routing
2025-12-17 10:06:36 +01:00

300 lines
11 KiB
TypeScript

import { test, expect } from '@playwright/test';
test('E2E 2FA Flow with German Localization and Dark Mode Email', async ({ page, context }) => {
const username = 'admin_' + Date.now();
const email = username + '@example.com';
const password = 'admin';
test.setTimeout(120000);
// Listen for console logs and errors
page.on('console', msg => console.log(`BROWSER LOG: ${msg.text()} `));
page.on('pageerror', exception => console.log(`BROWSER ERROR: ${exception} \nStack: ${exception.stack} `));
page.on('response', async response => {
if (response.status() >= 500) {
console.log(`BROWSER RESP ${response.status()}: ` + await response.text().catch(() => 'No Body'));
}
});
// 1. Reset Settings to ensure SMTP works (Backdoor)
await page.request.post('http://localhost:5001/api/debug/fix-settings');
// 1. Logic: Register or Login as Admin
await page.goto('http://localhost:5001');
// Wait for Auth Page to load (Login text or Register button)
try {
const loginText = page.locator('text="Sign In"'); // Try finding "Sign In" or "Login"
await expect(page.locator('form input[name="username"]')).toBeVisible({ timeout: 10000 });
} catch (e) {
console.log('Not on login page directly? checking...');
}
// Attempt to register first (fresh env)
let isLoggedIn = false;
try {
// Check if we are on auth page by looking for Register tab btn
const registerBtn = page.getByRole('button', { name: 'Register' });
// Check if username input is visible (meaning we are on Auth page)
if (await page.locator('input[name="username"]').isVisible()) {
if (await registerBtn.isVisible()) {
await registerBtn.click();
await page.fill('input[name="username"]', username);
await page.fill('input[name="email"]', email);
await page.fill('input[name="password"]', password);
await page.click('button[type="submit"]'); // Create Account
// Wait a bit to see if we logged in
try {
await expect(page.locator('[data-testid="fab-create-task"]').or(page.locator('[data-testid="logged-in-debug"]'))).toBeVisible({ timeout: 2000 });
isLoggedIn = true;
} catch (e) {
console.log('Registration did not log us in immediately. checking for errors or trying login...');
}
}
if (!isLoggedIn) {
// Try Login
console.log('Switching to Login...');
const loginBtn = page.getByRole('button', { name: 'Login' }); // Tab button
if (await loginBtn.isVisible()) await loginBtn.click();
await page.fill('input[name="username"]', username);
await page.fill('input[name="password"]', password);
await page.click('button[type="submit"]');
}
} else {
console.log('Username input not visible, assuming already logged in.');
isLoggedIn = true;
}
} catch (e) {
console.log('Auth flow error:', e);
}
// Verify we are actually logged in OR hit 2FA
try {
// Check for FAB (Logged in) OR 2FA Header
// Check for FAB (Logged in) OR Debug Div (LoggedIn) OR 2FA Header
const fab = page.locator('[data-testid="fab-create-task"]').or(page.locator('[data-testid="logged-in-debug"]'));
const twoFaHeader = page.locator('text=2FA Verification'); // English default
const twoFaHeaderDe = page.locator('text=2FA Verifizierung'); // German
// Wait for any of these
await Promise.race([
expect(fab).toBeVisible({ timeout: 5000 }),
expect(twoFaHeader).toBeVisible({ timeout: 5000 }),
expect(twoFaHeaderDe).toBeVisible({ timeout: 5000 })
]);
if (await twoFaHeader.isVisible() || await twoFaHeaderDe.isVisible()) {
console.log('Hit 2FA screen, solving...');
// Fetch Code
await page.waitForTimeout(2000);
const mailhogRes = await page.request.get('http://localhost:8025/api/v2/messages');
const messages = await mailhogRes.json();
const latestMessage = messages.items[0];
const body = latestMessage.Content.Body;
const code = body.match(/\d{6}/)[0];
await page.fill('input[type="text"]', code);
await page.click('button[type="submit"]'); // Verify
// Now wait for FAB
await expect(fab).toBeVisible({ timeout: 5000 });
} else {
console.log('Logged in directly (No 2FA)');
}
// Force Complete Routines to bypass Blocker
console.log('Force completing routines...');
await page.request.post('http://localhost:5001/api/user/routine/morning/complete');
await page.request.post('http://localhost:5001/api/user/routine/evening/complete');
// Disable Global Routine Blocker via API (Admin Settings)
console.log('Disabling Global Routine Blocker via API...');
await page.request.post('http://localhost:5001/api/admin/settings', {
data: {
evening_routine_enabled: "false",
morning_routine_enabled: "false",
smtp_host: "localhost",
smtp_port: "1025",
smtp_user: "",
smtp_pass: "",
smtp_from: "noreply@example.com",
smtp_secure: "false"
}
});
// Reload to pick up new settings
console.log('Reloading to apply settings...');
await page.reload();
await page.waitForTimeout(3000); // Wait for initialization
} catch (e) {
console.log('Current URL:', page.url());
throw new Error('Failed to login/register');
}
// 2. Logic: Ensure 2FA is Enabled & Set Language to German
await page.goto('http://localhost:5001/settings');
// Wait for loader to disappear
await expect(page.locator('.animate-spin')).toHaveCount(0, { timeout: 10000 });
// Verify we are on settings page.
try {
await expect(page.locator('[data-testid="text-settings-title"]')).toBeVisible({ timeout: 10000 });
} catch (e) {
console.log('Current URL:', page.url());
console.log('Page content snapshot:', await page.content());
throw e;
}
// Ensure Language is English first (to reliably find 2FA switch if using text) or use ID
// Ensure Language is English first (to reliably find 2FA switch if using text) or use ID
await page.addStyleTag({ content: 'vite-error-overlay { display: none !important; }' });
await page.click('[data-testid="select-language"]');
await page.click('[data-testid="option-language-en"]');
await page.waitForTimeout(500); // persist
// Enable 2FA if not enabled
// We can check the privacy API or just toggle.
// Let's toggle it ON.
// Switch ID/locating
// In settings.tsx loop:
// Use getByText to be more robust
const twoFaText = page.getByText('Two-Factor Authentication', { exact: false }).first();
try {
await expect(twoFaText).toBeVisible({ timeout: 5000 });
} catch (e) {
console.log('2FA Text not found. Page content:');
console.log(await page.content());
throw e;
}
// Find switch in the same container.
// Structure: div > [text], Switch
// We can go up to parent div.
const twoFaSwitch = page.locator('div.flex.items-center.justify-between').filter({ has: twoFaText }).getByRole('switch');
const isChecked = await twoFaSwitch.getAttribute('aria-checked') === 'true';
if (!isChecked) {
console.log('Enabling 2FA...');
await twoFaSwitch.click();
await page.waitForTimeout(1000); // persist
}
// Set Language to German
await page.click('[data-testid="select-language"]');
await page.click('[data-testid="option-language-de"]');
// Wait for persistence (API call)
await page.waitForTimeout(1000);
// 3. Logic: Logout
// Use sidebar logout or header logout?
// AppSidebar has logout button.
// We need to trigger sidebar if mobile, or just find the button.
// Button title="Logout"
// Assuming Sidebar is visible (desktop) or we open it.
// The test runs in desktop view by default in playwright config usually.
const logoutBtn = page.locator('button[title="Logout"]');
// If not visible, might be in a menu.
// Sidebar usually has it.
if (await logoutBtn.isVisible()) {
await logoutBtn.click();
} else {
// Try finding by icon or text "Logout" / "Abmelden"
// In German: "Abmelden"?
// Let's use URL fallback if UI fails
await page.goto('http://localhost:5001/api/logout');
// API logout returns 200 or redirect.
// But client state needs to be cleared?
// Better to use UI.
// Find "Log out" text?
// Sidebar footer user menu?
// Let's try locating any button with Log out text (or German Abmelden)
const logoutTextBtn = page.locator('button:has-text("Abmelden")');
if (await logoutTextBtn.isVisible()) {
await logoutTextBtn.click();
} else {
// Fallback: clear cookies/storage manually?
// No, let's assume Sidebar is there.
// Maybe checking 'button[data-testid="sidebar-logout"]' if added?
// If failing, let's force navigate
await page.goto('http://localhost:5001/auth?mode=login');
// But query cache might persist user?
// The app checks /api/user. If cookie is gone, it returns 401.
await context.clearCookies();
await page.reload();
}
}
await page.waitForURL(/.*\/auth/);
// 4. Logic: Login again to trigger 2FA (Now in German context)
await page.fill('input[name="username"]', username);
await page.fill('input[name="password"]', password);
// Capture response to get debug code if SMTP fails
const loginResponsePromise = page.waitForResponse(response => response.url().includes('/api/login') && response.request().method() === 'POST');
await page.click('button[type="submit"]');
const loginResponse = await loginResponsePromise;
const loginJson = await loginResponse.json();
const debugCode = loginJson.debugCode;
// Verify 2FA Screen - Should have German title "2FA-Verifizierung"
// Note: Translation key 'auth.2faVerification'.
// Ensure we check for the GERMAN text.
await expect(page.locator('text=2FA Verifizierung')).toBeVisible({ timeout: 5000 });
let code = debugCode;
if (!code) {
// 5. Logic: Fetch Code from MailHog
// Use existing request context
await page.waitForTimeout(2000);
const mailhogRes2 = await page.request.get('http://localhost:8025/api/v2/messages');
const messages2 = await mailhogRes2.json();
const latestMessage2 = messages2.items[0];
// Verify Email Subject and Body (German)
expect(latestMessage2.Content.Headers.Subject[0]).toContain('Ihr 2FA-Verifizierungscode');
const emailBody = latestMessage2.Content.Body;
// Verify Dark Mode (HTML check)
expect(emailBody).toContain('background-color: #09090b'); // Dark background
expect(emailBody).toContain('#fafafa'); // Light text
expect(emailBody).toContain('Verifizierungscode');
code = emailBody.match(/\d{6}/)[0];
console.log('Got German 2FA Code from MailHog:', code);
} else {
console.log('Using Debug 2FA Code from response:', code);
}
// 6. Logic: Enter Code
await page.getByPlaceholder('123456').fill(code);
// Button text might be "Überprüfen" or "Verifizieren"
// We can use the button type submit, or look for the text.
// In German de.json, auth.verify usually translates to "Verifizieren" or "Bestätigen"
// Let's use the submit button generic locator since it's the only one
await page.click('button[type="submit"]');
// 7. Logic: Success
await page.waitForTimeout(3000);
await expect(page).toHaveURL('http://localhost:5001/');
// 8. Restore Language to English for future tests (Optional)
await page.goto('http://localhost:5001/settings');
await page.click('[data-testid="select-language"]');
await page.click('[data-testid="option-language-en"]');
});