Files
Charon/tests/core/admin-onboarding.spec.ts

334 lines
14 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect, logoutUser, TEST_PASSWORD } from '../fixtures/auth-fixtures';
import { waitForAPIResponse, waitForLoadingComplete } from '../utils/wait-helpers';
/**
* Admin Onboarding & Setup Workflow
*
* Purpose: Validate that first-time admin can successfully set up the system
* Scenarios: Login, dashboard display, settings access, emergency token generation
* Success: All admin UI flows work without errors, data persists
*/
test.describe('Admin Onboarding & Setup', () => {
// Purpose: Establish baseline admin auth state before each test
// Fixture ensures admin is logged in and authenticated
test.beforeEach(async ({ page, adminUser }, testInfo) => {
const shouldSkipLogin = /Admin logs in with valid credentials/i.test(testInfo.title);
await page.goto(`/`, { waitUntil: 'domcontentloaded' });
if (shouldSkipLogin) {
await page.context().clearCookies();
await page.evaluate(() => {
localStorage.clear();
sessionStorage.clear();
});
await page.goto(`/login`, { waitUntil: 'domcontentloaded' });
return;
}
await page.goto(`/login`, { waitUntil: 'domcontentloaded' });
const emailInput = page.locator('input[type="email"], input[name="email"], input[autocomplete="email"], input[placeholder*="@"]');
const passwordInput = page.locator('input[type="password"], input[name="password"], input[autocomplete="current-password"]');
await expect(emailInput.first()).toBeVisible({ timeout: 15000 });
await emailInput.first().fill(adminUser.email);
await passwordInput.first().fill(TEST_PASSWORD);
const loginButton = page.getByRole('button', { name: /login|sign in/i });
const responsePromise = waitForAPIResponse(page, '/api/v1/auth/login', { status: 200 });
await loginButton.click();
await responsePromise;
await page.waitForURL(/\/dashboard|\/admin|\/$/, { timeout: 15000 });
await waitForLoadingComplete(page, { timeout: 15000 });
await expect(page.getByRole('main')).toBeVisible({ timeout: 15000 });
});
// Admin logs in with valid credentials
test('Admin logs in with valid credentials', async ({ page, adminUser }) => {
const start = Date.now();
await test.step('Navigate to login page', async () => {
await page.goto(`/login`, { waitUntil: 'domcontentloaded' });
const emailField = page.locator('input[type="email"], input[name="email"], input[autocomplete="email"], input[placeholder*="@"]');
await expect(emailField.first()).toBeVisible({ timeout: 15000 });
});
await test.step('Enter credentials and submit', async () => {
const emailInput = page.locator('input[type="email"], input[name="email"], input[autocomplete="email"], input[placeholder*="@"]');
const passwordInput = page.locator('input[type="password"], input[name="password"], input[autocomplete="current-password"]');
expect(emailInput).toBeDefined();
expect(passwordInput).toBeDefined();
await emailInput.first().fill(adminUser.email);
await passwordInput.first().fill(TEST_PASSWORD);
const loginButton = page.getByRole('button', { name: /login|sign in/i });
const responsePromise = waitForAPIResponse(page, '/api/v1/auth/login', { status: 200 });
await loginButton.click();
await responsePromise;
});
await test.step('Verify successful authentication', async () => {
// Wait for dashboard to load (indicates successful auth)
await page.waitForURL(/\/dashboard|\/admin|\/[^/]*$/, { timeout: 10000 });
await waitForLoadingComplete(page, { timeout: 15000 });
await expect(page.getByRole('main')).toBeVisible();
const duration = Date.now() - start;
console.log(`✓ Admin login completed in ${duration}ms`);
});
});
// Dashboard displays after login
test('Dashboard displays after login', async ({ page }) => {
await test.step('Navigate to dashboard', async () => {
await page.goto(`/`, { waitUntil: 'domcontentloaded' });
await waitForLoadingComplete(page, { timeout: 15000 });
});
await test.step('Verify dashboard widgets render', async () => {
const mainContent = page.getByRole('main');
await expect(mainContent).toBeVisible({ timeout: 15000 });
const dashboardHeading = page.getByRole('heading', { name: /dashboard/i }).first();
await expect(dashboardHeading).toBeVisible({ timeout: 15000 });
const proxyHostsLink = page.getByRole('link', { name: /proxy hosts/i }).first();
await expect(proxyHostsLink).toBeVisible({ timeout: 15000 });
});
await test.step('Verify user info displayed', async () => {
// Admin name or email should be visible in header/profile area
const accountLink = page.locator('a[href*="settings/account"]').first();
await expect(accountLink).toBeVisible({ timeout: 15000 });
});
});
// System settings accessible from menu
test('System settings accessible from menu', async ({ page }) => {
await test.step('Open settings menu', async () => {
// Look for settings link in navigation
const settingsLink = page.getByRole('link', { name: /settings|configuration/i }).first();
if (await settingsLink.isVisible().catch(() => false)) {
await settingsLink.click();
} else {
// Try menu button approach
const menuButton = page.getByRole('button', { name: /menu/i }).first();
if (await menuButton.isVisible().catch(() => false)) {
await menuButton.click();
}
const menuSettingsLink = page.getByRole('link', { name: /settings|configuration/i }).first();
if (await menuSettingsLink.isVisible().catch(() => false)) {
await menuSettingsLink.click();
} else {
await page.goto(`/settings`, { waitUntil: 'domcontentloaded' });
}
}
});
await test.step('Verify settings page loads', async () => {
await waitForLoadingComplete(page, { timeout: 15000 });
const settingsHeading = page.getByRole('heading', { name: /setting|configuration/i }).first();
await expect(settingsHeading).toBeVisible({ timeout: 15000 });
});
await test.step('Verify settings form elements present', async () => {
// At minimum, should have some form inputs
const inputs = page.locator('input, select, textarea').first();
await expect(inputs).toBeVisible();
});
});
// Emergency token can be generated
test('Emergency token can be generated', async ({ page }) => {
await test.step('Navigate to security settings', async () => {
await page.goto('/settings/security', { waitUntil: 'domcontentloaded' }).catch(() => {
// Fallback: click through menu
return page.goto('/settings');
});
await waitForLoadingComplete(page, { timeout: 15000 });
});
await test.step('Find emergency token section', async () => {
await waitForLoadingComplete(page, { timeout: 15000 });
const emergencySection = page.getByText(/admin whitelist|emergency|break.?glass|recovery token/i).first();
const isVisible = await emergencySection.isVisible().catch(() => false);
if (isVisible) {
await expect(emergencySection).toBeVisible();
}
});
await test.step('Generate emergency token', async () => {
const sectionHeading = page.getByRole('heading', { name: /admin whitelist/i }).first();
const sectionContainer = sectionHeading.locator('..');
const scopedGenerateButton = sectionContainer.getByRole('button', { name: /generate token/i });
const fallbackGenerateButton = page.getByRole('button', { name: /generate token/i }).first();
const generateButton = (await scopedGenerateButton.isVisible().catch(() => false))
? scopedGenerateButton
: fallbackGenerateButton;
const isGenerateVisible = await generateButton.isVisible().catch(() => false);
if (!isGenerateVisible) {
test.skip(true, 'Generate Token button not available in this deployment');
return;
}
await generateButton.click();
// Wait for modal or confirmation
await page.waitForSelector('[role="dialog"], [class*="modal"]', { timeout: 5000 }).catch(() => {
// Modal might not exist, token might appear inline
return Promise.resolve();
});
});
await test.step('Verify token displayed and copyable', async () => {
// Token input or display field
const tokenField = page.locator('input[readonly], [data-testid="emergency-token"], [class*="token"]').first();
await expect(tokenField).toBeVisible();
// Should have copy button
const copyButton = page.getByRole('button', { name: /copy|clipboard/i });
if (await copyButton.isVisible()) {
await copyButton.click();
// Verify feedback (toast, button change, etc.)
}
});
});
// Encryption key setup required on first login
test('Dashboard loads with encryption key management', async ({ page }) => {
await test.step('Navigate to encryption settings', async () => {
await page.goto(`/settings/encryption`, { waitUntil: 'networkidle' }).catch(() => {
return page.goto(`/settings`);
});
});
await test.step('Verify encryption options present', async () => {
const encryptionSection = page.getByText(/encryption|cipher|passphrase/i).first();
// May or may not be visible depending on setup state
const encryptionForm = page.locator('[data-testid="encryption-form"], [class*="encryption"]').first();
if (await encryptionForm.isVisible()) {
await expect(encryptionForm).toBeVisible();
}
});
await test.step('Verify form is interactive', async () => {
const inputs = page.locator('input[type="password"], input[type="text"]');
const inputCount = await inputs.count();
expect(inputCount).toBeGreaterThanOrEqual(0); // May be 0 if already set
});
});
// Navigation menu items all functional
test('Navigation menu items all functional', async ({ page }) => {
const menuItems = [
{ name: /dashboard|home/i, path: /dashboard|^\/$/i },
{ name: /proxy|proxy.?hosts/i, path: /proxy|host/i },
{ name: /user|user.?management/i, path: /user|people/i },
{ name: /domain|dns/i, path: /domain|dns/i },
{ name: /setting|config/i, path: /setting|config/i },
];
for (const item of menuItems) {
// Skip if menu item not found (might not be in scope for all deployments)
const menuLink = page.getByRole('link', { name: item.name }).first();
if (!(await menuLink.isVisible())) {
console.log(` Menu item '${item.name}' not found (may not be in scope)`);
continue;
}
await test.step(`Navigate to ${item.name}`, async () => {
await menuLink.click();
// Verify page loaded
await page.waitForLoadState('networkidle').catch(() => Promise.resolve());
const url = page.url();
// Don't strictly enforce URL pattern - just verify page loaded
expect(url).toBeTruthy();
});
}
});
// Logout clears session
test('Logout clears session', async ({ page }) => {
let initialStorageSize = 0;
await test.step('Note initial storage state', async () => {
initialStorageSize = (await page.evaluate(() => {
return Object.keys(localStorage).length + Object.keys(sessionStorage).length;
})) || 0;
expect(initialStorageSize).toBeGreaterThan(0); // Should have auth data
});
await test.step('Click logout button', async () => {
// Look for logout in user menu
const profileMenu = page.locator('[data-testid="user-menu"], [class*="profile"], [class*="avatar"]').first();
if (await profileMenu.isVisible()) {
await profileMenu.click();
}
const logoutButton = page.getByRole('menuitem', { name: /logout|sign out/i })
.or(page.getByRole('button', { name: /logout|sign out/i }))
.first();
await logoutButton.click();
});
await test.step('Verify redirected to login', async () => {
await page.waitForURL(/login|signin|^\/$/i, { timeout: 5000 });
const currentPath = page.url();
expect(currentPath).toMatch(/login|signin|auth/i);
});
await test.step('Verify session storage cleared', async () => {
const currentStorageSize = (await page.evaluate(() => {
return Object.keys(localStorage).length + Object.keys(sessionStorage).length;
})) || 0;
// Storage should be smaller (auth tokens removed)
// Note: This is a soft check - some persistent storage might remain
expect(currentStorageSize).toBeLessThanOrEqual(initialStorageSize);
});
});
// Re-login after logout successful
test('Re-login after logout successful', async ({ page, adminUser }) => {
await test.step('Ensure we are logged out', async () => {
await logoutUser(page);
await page.goto(`/login`, { waitUntil: 'domcontentloaded' });
});
await test.step('Perform login again', async () => {
const emailInput = page.locator('input[type="email"], input[name="email"], input[autocomplete="email"], input[placeholder*="@"]');
const passwordInput = page.locator('input[type="password"], input[name="password"], input[autocomplete="current-password"]');
await emailInput.first().fill(adminUser.email);
await passwordInput.first().fill(TEST_PASSWORD);
const loginButton = page.getByRole('button', { name: /login|sign in|submit/i });
await loginButton.click();
});
await test.step('Verify new session established', async () => {
await page.waitForURL(/dashboard|admin|[^/]*$/, { timeout: 10000 });
const storageAuth = await page.evaluate(() => {
return localStorage.getItem('auth') || localStorage.getItem('token') || 'exists';
});
// Should have auth in storage
expect(storageAuth).toBeTruthy();
});
await test.step('Verify dashboard accessible', async () => {
await waitForLoadingComplete(page, { timeout: 15000 });
const mainContent = page.getByRole('main');
await expect(mainContent).toBeVisible({ timeout: 15000 });
});
});
});