import { test, expect } from '@playwright/test'; /** * E2E Tests for Multi-Server Login Flow (US-012) * * These tests verify the simplified multi-server login flow. * Tests use route interception to mock API responses for predictable multi-server scenarios. * * Prerequisites: * 1. Frontend running on port 3000 (./start.sh test) * 2. Tests mock API responses - no real multi-server backend needed * * Run: * npm run test:e2e -- multi-server-login.spec.js * npm run test:e2e:headed -- multi-server-login.spec.js */ // Mock data for multi-server scenarios const MOCK_SERVERS = { romfast: { id: 'romfast', name: 'Romfast - Producție' }, dev: { id: 'dev', name: 'Development Server' }, test: { id: 'test', name: 'Test Environment' }, }; const MOCK_USER = { user_id: 1, username: 'test@example.com', full_name: 'Test User', companies: [{ id_firma: 1, denumire: 'Test Company' }], permissions: ['view_reports'], }; // Generate a valid-looking JWT token for testing const generateMockJWT = (serverId = null) => { const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' })); const payload = btoa(JSON.stringify({ sub: 'test@example.com', user_id: 1, username: 'test@example.com', server_id: serverId, companies: [{ id_firma: 1, denumire: 'Test Company' }], permissions: ['view_reports'], exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000), type: 'access', })); const signature = 'mock_signature_for_testing'; return `${header}.${payload}.${signature}`; }; /** * Helper to wait for server dropdown to become enabled (servers loaded) */ async function waitForServersLoaded(page, timeout = 5000) { await page.waitForFunction( () => { const dropdown = document.querySelector('#server'); return dropdown && !dropdown.classList.contains('p-disabled'); }, { timeout } ); } /** * Setup route interception for multi-server mode */ async function setupMultiServerMocks(page, options = {}) { const { authMode = 'multi-server', emailExists = true, serverCount = 2, loginSuccess = true, loginError = null, } = options; // Mock /api/system/auth-mode endpoint await page.route('**/api/system/auth-mode', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ mode: authMode, supports_email_login: authMode === 'multi-server', }), }); }); // Mock both /api/auth/check-email (legacy) and /api/auth/check-identity (US-013) const handleIdentityCheck = async (route) => { if (!emailExists) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ exists: false, servers: [], identity_type: 'unknown', }), }); return; } const servers = []; if (serverCount >= 1) servers.push(MOCK_SERVERS.romfast); if (serverCount >= 2) servers.push(MOCK_SERVERS.dev); if (serverCount >= 3) servers.push(MOCK_SERVERS.test); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ exists: true, servers: servers, identity_type: 'email', }), }); }; // Register both endpoints for backward compatibility await page.route('**/api/auth/check-email', handleIdentityCheck); await page.route('**/api/auth/check-identity', handleIdentityCheck); // Mock /api/auth/login endpoint await page.route('**/api/auth/login', async (route) => { // Parse request body to get server_id const body = route.request().postDataJSON(); const serverId = body?.server_id || null; if (!loginSuccess) { await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ detail: loginError || 'Invalid credentials', }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ access_token: generateMockJWT(serverId), refresh_token: generateMockJWT(serverId).replace('access', 'refresh'), token_type: 'bearer', user: MOCK_USER, }), }); }); } test.describe('Multi-Server Login Flow (US-012)', () => { test.describe('AC1: Email exists on 1 server - direct login without dropdown', () => { test('should auto-select server when email exists on single server', async ({ page }) => { // Setup mocks for single-server scenario await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 1, loginSuccess: true, }); await page.goto('/'); // Wait for form with identity field await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and trigger blur to load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await page.waitForTimeout(500); // Password field should be visible and enabled await page.waitForSelector('#password', { timeout: 5000 }); const passwordInput = page.locator('#password input'); await expect(passwordInput).toBeEnabled(); // Server dropdown should be visible but with single server auto-selected const serverDropdown = page.locator('#server'); await expect(serverDropdown).toBeVisible(); }); // TODO: Fix mock interception issue - login mock may not be working correctly test.skip('should complete login when email exists on single server', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 1, loginSuccess: true, }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and trigger blur to load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load (dropdown becomes enabled) await waitForServersLoaded(page); // Fill password await page.locator('#password input').fill('testpassword'); // Click login await page.click('button:has-text("Autentificare")'); // Should redirect away from login await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 10000 }); // Wait a moment for localStorage to be updated await page.waitForTimeout(200); // Verify JWT is stored const token = await page.evaluate(() => localStorage.getItem('access_token')); expect(token).toBeTruthy(); expect(token.split('.').length).toBe(3); }); }); test.describe('AC2: Email exists on 2+ servers - dropdown appears', () => { test('should show server dropdown when email exists on multiple servers', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 2, loginSuccess: true, }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Server dropdown should be visible but disabled initially const serverDropdown = page.locator('#server'); await expect(serverDropdown).toBeVisible(); await expect(serverDropdown).toHaveClass(/p-disabled/); // Enter username and trigger blur to load servers await page.fill('input#identity', 'multiuser@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await page.waitForTimeout(500); // Verify dropdown is now enabled (servers loaded) - no p-disabled class await expect(serverDropdown).not.toHaveClass(/p-disabled/); }); test('should allow server selection and complete login', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 2, loginSuccess: true, }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and trigger blur await page.fill('input#identity', 'multiuser@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Click dropdown to open options await page.click('#server'); // Select second server (Development Server) await page.click('li:has-text("Development Server")'); // Fill password const passwordInput = page.locator('#password input'); await expect(passwordInput).toBeEnabled(); await passwordInput.fill('testpassword'); // Button should be enabled const submitButton = page.locator('button:has-text("Autentificare")'); await expect(submitButton).toBeEnabled(); // Submit await submitButton.click(); // Should redirect await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 10000 }); }); test('should complete login with selected server', async ({ page }) => { let capturedServerId = null; // Custom mock to capture server_id await page.route('**/api/system/auth-mode', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ mode: 'multi-server', supports_email_login: true }), }); }); // Mock both endpoints for compatibility const handleIdentityCheck = async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ exists: true, servers: [MOCK_SERVERS.romfast, MOCK_SERVERS.dev], identity_type: 'email', }), }); }; await page.route('**/api/auth/check-email', handleIdentityCheck); await page.route('**/api/auth/check-identity', handleIdentityCheck); await page.route('**/api/auth/login', async (route) => { const body = route.request().postDataJSON(); capturedServerId = body?.server_id; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ access_token: generateMockJWT(capturedServerId), refresh_token: generateMockJWT(capturedServerId), token_type: 'bearer', user: MOCK_USER, }), }); }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and load servers await page.fill('input#identity', 'multiuser@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Select server await page.click('#server'); await page.click('li:has-text("Development Server")'); // Fill password and submit await page.locator('#password input').fill('testpassword'); await page.click('button:has-text("Autentificare")'); // Wait for login to complete await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 10000 }); // Wait for page to stabilize after navigation await page.waitForTimeout(500); // Verify server_id was sent in login request expect(capturedServerId).toBe('dev'); // Verify last_server_id is saved const lastServerId = await page.evaluate(() => localStorage.getItem('last_server_id')); expect(lastServerId).toBe('dev'); }); test('should pre-select last used server from localStorage', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 2, loginSuccess: true, }); // Pre-set last_server_id in localStorage await page.addInitScript(() => { localStorage.setItem('last_server_id', 'dev'); }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and trigger blur await page.fill('input#identity', 'multiuser@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Verify that 'dev' server is pre-selected const selectedValue = await page.locator('#server .p-dropdown-label').textContent(); expect(selectedValue).toContain('Development Server'); }); }); test.describe('AC3: Unknown email - server dropdown stays empty', () => { test('should keep server dropdown disabled for unknown username', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: false, serverCount: 0, }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter unknown username and trigger blur await page.fill('input#identity', 'unknown@example.com'); await page.locator('input#identity').blur(); // Wait for response await page.waitForTimeout(500); // Server dropdown should be visible but disabled (no servers) - check PrimeVue class const serverDropdown = page.locator('#server'); await expect(serverDropdown).toBeVisible(); await expect(serverDropdown).toHaveClass(/p-disabled/); }); test('should NOT expose server names for unknown username', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: false, serverCount: 0, }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter unknown username and trigger blur await page.fill('input#identity', 'hacker@evil.com'); await page.locator('input#identity').blur(); // Wait for response await page.waitForTimeout(500); // No server names should be visible anywhere const pageContent = await page.textContent('body'); expect(pageContent).not.toContain('Romfast'); expect(pageContent).not.toContain('Development Server'); expect(pageContent).not.toContain('Test Environment'); }); }); test.describe('AC4: Wrong password - clear error message', () => { // TODO: Fix mock interception issue test.skip('should show "Parolă incorectă" for wrong password', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 1, loginSuccess: false, loginError: 'Invalid password', }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Fill wrong password and submit await page.locator('#password input').fill('wrongpassword'); await page.click('button:has-text("Autentificare")'); // Wait for error await page.waitForTimeout(1000); // Check for error toast const errorToast = page.locator('.p-toast-message-error, .p-toast-message'); await expect(errorToast).toBeVisible({ timeout: 5000 }); await expect(errorToast).toContainText(/incorect|greșit|wrong|invalid/i); }); // TODO: Fix mock interception issue test.skip('should show "Cont inactiv" for inactive account', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 1, loginSuccess: false, loginError: 'Account is inactive', }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and load servers await page.fill('input#identity', 'inactive@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Fill password and submit await page.locator('#password input').fill('password123'); await page.click('button:has-text("Autentificare")'); // Wait for error await page.waitForTimeout(1000); // Check for error toast const errorToast = page.locator('.p-toast-message-error, .p-toast-message'); await expect(errorToast).toBeVisible({ timeout: 5000 }); await expect(errorToast).toContainText(/inactiv|disabled|blocat/i); }); // TODO: Fix mock interception issue test.skip('should keep form state after login error', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 1, loginSuccess: false, loginError: 'Invalid credentials', }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Fill password and submit await page.locator('#password input').fill('badpassword'); await page.click('button:has-text("Autentificare")'); // Wait for error await page.waitForTimeout(1000); // Form should remain visible with all fields const passwordField = page.locator('#password'); await expect(passwordField).toBeVisible(); // Identity field should still show the email const identityValue = await page.locator('input#identity').inputValue(); expect(identityValue).toBe('user@example.com'); }); }); test.describe('AC5: JWT contains server_id after login', () => { // TODO: Fix mock interception issue - token not saved to localStorage test.skip('should include server_id in JWT token payload', async ({ page }) => { const selectedServerId = 'romfast'; await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 1, // Single server for simpler test loginSuccess: true, }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Fill password and submit await page.locator('#password input').fill('password123'); await page.click('button:has-text("Autentificare")'); // Wait for login completion await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 10000 }); // Get JWT from localStorage and decode it const token = await page.evaluate(() => localStorage.getItem('access_token')); expect(token).toBeTruthy(); // Decode JWT payload (base64) const payloadB64 = token.split('.')[1]; const payloadJson = await page.evaluate((b64) => { return JSON.parse(atob(b64)); }, payloadB64); // Verify server_id is in the payload expect(payloadJson).toHaveProperty('server_id'); expect(payloadJson.server_id).toBe(selectedServerId); }); // TODO: Fix navigation timeout issue when navigating to /reports after mock login test.skip('should send server_id in subsequent API requests', async ({ page }) => { let apiRequestHeaders = null; await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 1, loginSuccess: true, }); // Intercept any API call after login to capture headers await page.route('**/api/reports/**', async (route) => { apiRequestHeaders = route.request().headers(); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }), }); }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Fill password and submit await page.locator('#password input').fill('password123'); await page.click('button:has-text("Autentificare")'); await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 10000 }); // Try to navigate to reports (which should trigger an API call) await page.goto('/reports'); await page.waitForTimeout(2000); // If an API call was made, verify Authorization header if (apiRequestHeaders && apiRequestHeaders.authorization) { expect(apiRequestHeaders.authorization).toMatch(/^Bearer /); // The JWT in Authorization header should contain server_id const token = apiRequestHeaders.authorization.replace('Bearer ', ''); const payloadB64 = token.split('.')[1]; const payloadJson = await page.evaluate((b64) => { return JSON.parse(atob(b64)); }, payloadB64); expect(payloadJson).toHaveProperty('server_id'); } }); }); test.describe('Form behavior', () => { test('should reset servers when username is modified', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', emailExists: true, serverCount: 2, }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Server dropdown should be enabled (no p-disabled class) const serverDropdown = page.locator('#server'); await expect(serverDropdown).not.toHaveClass(/p-disabled/); // Modify the username await page.fill('input#identity', 'different@example.com'); // Server dropdown should be disabled again (servers cleared) - check PrimeVue class await expect(serverDropdown).toHaveClass(/p-disabled/); }); }); }); test.describe('Auth Mode Detection', () => { test('multi-server mode shows server dropdown', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // In multi-server mode, server dropdown should be visible const serverDropdown = page.locator('#server'); await expect(serverDropdown).toBeVisible(); }); test('single-server mode hides server dropdown', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'single-server', }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // In single-server mode, server dropdown should NOT be visible const serverDropdown = page.locator('#server'); await expect(serverDropdown).not.toBeVisible(); }); test('form shows fixed "Utilizator" label', async ({ page }) => { await setupMultiServerMocks(page, { authMode: 'multi-server', }); await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Label should always be "Utilizator" regardless of mode await expect(page.locator('label[for="identity"]')).toContainText(/utilizator/i); }); }); /** * US-011: Multi-Server Company Persistence Fix Tests * * These tests verify that company selection is properly isolated between servers. * Bug fix: When switching servers, company from server A should NOT be restored on server B. * * Implementation details (from US-003): * - localStorage key includes server_id: selected_company_${username}_${serverId} * - Saved company object includes _server_id for validation at restore * - loadCompanies() validates _server_id before restoring saved company */ test.describe('Multi-Server Company Persistence Fix (US-011)', () => { // Mock companies for different servers const SERVER_A_COMPANIES = [ { id_firma: 1, name: 'Company A1', denumire: 'Company A1' }, { id_firma: 2, name: 'Company A2', denumire: 'Company A2' }, ]; const SERVER_B_COMPANIES = [ { id_firma: 101, name: 'Company B1', denumire: 'Company B1' }, { id_firma: 102, name: 'Company B2', denumire: 'Company B2' }, ]; // Companies with same id_firma on different servers (edge case) const SERVER_C_COMPANIES = [ { id_firma: 1, name: 'Same ID Company on Server C', denumire: 'Same ID Company on Server C' }, { id_firma: 200, name: 'Company C2', denumire: 'Company C2' }, ]; /** * Setup mocks for multi-server company persistence tests */ async function setupCompanyPersistenceMocks(page, options = {}) { const { currentServerId = 'server_a', companies = SERVER_A_COMPANIES, } = options; // Mock /api/system/auth-mode endpoint await page.route('**/api/system/auth-mode', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ mode: 'multi-server', supports_email_login: true, }), }); }); // Mock both /api/auth/check-email and /api/auth/check-identity const handleIdentityCheck = async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ exists: true, servers: [ { id: 'server_a', name: 'Server A' }, { id: 'server_b', name: 'Server B' }, { id: 'server_c', name: 'Server C' }, ], identity_type: 'email', }), }); }; await page.route('**/api/auth/check-email', handleIdentityCheck); await page.route('**/api/auth/check-identity', handleIdentityCheck); // Mock /api/auth/login endpoint await page.route('**/api/auth/login', async (route) => { const body = route.request().postDataJSON(); const serverId = body?.server_id || 'server_a'; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ access_token: generateMockJWT(serverId), refresh_token: generateMockJWT(serverId), token_type: 'bearer', user: { user_id: 1, username: 'testuser@example.com', full_name: 'Test User', server_id: serverId, server_name: serverId === 'server_a' ? 'Server A' : serverId === 'server_b' ? 'Server B' : 'Server C', companies: companies, permissions: ['view_reports'], }, }), }); }); // Mock /api/companies endpoint - returns companies based on current server await page.route('**/api/companies', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ companies: companies, }), }); }); // Mock /api/auth/my-servers endpoint (for header dropdown) await page.route('**/api/auth/my-servers', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ servers: [ { id: 'server_a', name: 'Server A' }, { id: 'server_b', name: 'Server B' }, { id: 'server_c', name: 'Server C' }, ], }), }); }); // Mock any other API calls to prevent errors await page.route('**/api/reports/**', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ data: [] }), }); }); await page.route('**/api/accounting-periods/**', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ periods: [] }), }); }); } /** * Helper to perform login flow */ async function performLogin(page, serverId = 'server_a') { await page.goto('/'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter email await page.fill('input#identity', 'testuser@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Select server await page.click('#server'); const serverName = serverId === 'server_a' ? 'Server A' : serverId === 'server_b' ? 'Server B' : 'Server C'; await page.click(`li:has-text("${serverName}")`); // Fill password and submit await page.locator('#password input').fill('testpassword'); await page.click('button:has-text("Autentificare")'); // Wait for redirect await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 10000 }); } test('should save company with _server_id in localStorage', async ({ page }) => { await setupCompanyPersistenceMocks(page, { currentServerId: 'server_a', companies: SERVER_A_COMPANIES, }); await performLogin(page, 'server_a'); // Wait for companies to load await page.waitForTimeout(1000); // Check localStorage for saved company const savedCompanyKey = await page.evaluate(() => { return Object.keys(localStorage).find(key => key.startsWith('selected_company_') && key.includes('testuser') ); }); expect(savedCompanyKey).toBeTruthy(); // Verify the saved company has _server_id const savedCompanyJson = await page.evaluate((key) => { return localStorage.getItem(key); }, savedCompanyKey); const savedCompany = JSON.parse(savedCompanyJson); expect(savedCompany).toHaveProperty('_server_id'); expect(savedCompany._server_id).toBe('server_a'); }); test('should NOT restore company from different server', async ({ page }) => { // Pre-set a company from server_a in localStorage await page.addInitScript(() => { // Simulate saved company from server A const companyFromServerA = { id_firma: 1, name: 'Company A1', denumire: 'Company A1', _server_id: 'server_a' }; localStorage.setItem( 'selected_company_testuser@example.com_server_a', JSON.stringify(companyFromServerA) ); // Also set last_server_id to server_a localStorage.setItem('last_server_id', 'server_a'); }); // Setup mocks for server_b with different companies await setupCompanyPersistenceMocks(page, { currentServerId: 'server_b', companies: SERVER_B_COMPANIES, }); // Login to server_b await performLogin(page, 'server_b'); // Wait for companies to load await page.waitForTimeout(1500); // Check that the selected company is from server_b, NOT server_a const selectedCompanyKey = await page.evaluate(() => { return Object.keys(localStorage).find(key => key.includes('selected_company_testuser') && key.includes('server_b') ); }); // Should have a new key for server_b expect(selectedCompanyKey).toContain('server_b'); // If a company is saved, it should be from server_b's company list if (selectedCompanyKey) { const savedCompanyJson = await page.evaluate((key) => { return localStorage.getItem(key); }, selectedCompanyKey); if (savedCompanyJson) { const savedCompany = JSON.parse(savedCompanyJson); // Company id should be from SERVER_B_COMPANIES (101 or 102), not SERVER_A (1 or 2) expect([101, 102]).toContain(savedCompany.id_firma); expect(savedCompany._server_id).toBe('server_b'); } } }); test('should isolate companies when servers have same id_firma', async ({ page }) => { // Edge case: Server A and Server C both have a company with id_firma=1 // This test verifies that _server_id validation prevents cross-server restoration // Pre-set company from server_a with id_firma=1 await page.addInitScript(() => { const companyFromServerA = { id_firma: 1, name: 'Company A1', denumire: 'Company A1', _server_id: 'server_a' }; localStorage.setItem( 'selected_company_testuser@example.com_server_a', JSON.stringify(companyFromServerA) ); }); // Setup mocks for server_c which also has a company with id_firma=1 await setupCompanyPersistenceMocks(page, { currentServerId: 'server_c', companies: SERVER_C_COMPANIES, }); // Login to server_c await performLogin(page, 'server_c'); // Wait for companies to load and state to settle await page.waitForTimeout(1500); // Get the selected company for server_c const serverCCompanyJson = await page.evaluate(() => { const key = Object.keys(localStorage).find(k => k.includes('selected_company_testuser') && k.includes('server_c') ); return key ? localStorage.getItem(key) : null; }); if (serverCCompanyJson) { const serverCCompany = JSON.parse(serverCCompanyJson); // Should have _server_id of 'server_c', not 'server_a' // Even though both have id_firma=1 expect(serverCCompany._server_id).toBe('server_c'); // The company name should be from server C expect(serverCCompany.name).toBe('Same ID Company on Server C'); } // Verify server_a company is still intact const serverACompanyJson = await page.evaluate(() => { return localStorage.getItem('selected_company_testuser@example.com_server_a'); }); if (serverACompanyJson) { const serverACompany = JSON.parse(serverACompanyJson); expect(serverACompany._server_id).toBe('server_a'); expect(serverACompany.name).toBe('Company A1'); } }); test('should use server-specific localStorage keys', async ({ page }) => { await setupCompanyPersistenceMocks(page, { currentServerId: 'server_a', companies: SERVER_A_COMPANIES, }); await performLogin(page, 'server_a'); await page.waitForTimeout(1000); // Verify the localStorage key format includes server_id const keys = await page.evaluate(() => { return Object.keys(localStorage).filter(k => k.startsWith('selected_company_')); }); // Should have format: selected_company_${username}_${serverId} const serverSpecificKey = keys.find(k => k.includes('server_a')); expect(serverSpecificKey).toBeTruthy(); expect(serverSpecificKey).toMatch(/selected_company_.*_server_a/); }); test('should not show company not found error on server switch', async ({ page }) => { // This test verifies the original bug is fixed: // Switching servers should not cause "company not found" errors // Pre-set company from server_a await page.addInitScript(() => { const companyFromServerA = { id_firma: 999, // ID that doesn't exist on server_b name: 'Company Only On A', denumire: 'Company Only On A', _server_id: 'server_a' }; localStorage.setItem( 'selected_company_testuser@example.com_server_a', JSON.stringify(companyFromServerA) ); }); // Setup mocks for server_b await setupCompanyPersistenceMocks(page, { currentServerId: 'server_b', companies: SERVER_B_COMPANIES, }); // Collect console errors const consoleErrors = []; page.on('console', msg => { if (msg.type() === 'error') { consoleErrors.push(msg.text()); } }); // Login to server_b await performLogin(page, 'server_b'); // Wait for everything to load await page.waitForTimeout(2000); // There should be no "company not found" errors const companyNotFoundErrors = consoleErrors.filter(err => err.toLowerCase().includes('company not found') || err.toLowerCase().includes('company') && err.toLowerCase().includes('error') ); expect(companyNotFoundErrors).toHaveLength(0); // A company from server_b should be selected (auto-select first) const selectedCompanyJson = await page.evaluate(() => { const key = Object.keys(localStorage).find(k => k.includes('selected_company_testuser') && k.includes('server_b') ); return key ? localStorage.getItem(key) : null; }); if (selectedCompanyJson) { const selectedCompany = JSON.parse(selectedCompanyJson); // Should be from server_b's company list expect([101, 102]).toContain(selectedCompany.id_firma); } }); }); /** * US-012: URL Bookmark Server Pre-selection Tests * * These tests verify that the URL query parameter ?server=xyz correctly * pre-selects the specified server in the login form. * * Implementation details (from US-004 and US-005): * - LoginView reads route.query.server in onMounted * - Calls authStore.setPreselectedServer(serverId) if found * - checkIdentity() uses priority: preselectedServerId > lastServer > servers[0] * - Validates server against available servers before pre-selection */ test.describe('URL Bookmark Server Pre-selection (US-012)', () => { /** * Setup mocks for URL pre-selection tests */ async function setupURLPreselectionMocks(page, options = {}) { const { emailExists = true, servers = [ { id: 'romfast', name: 'Romfast - Producție' }, { id: 'dev', name: 'Development Server' }, { id: 'test_server', name: 'Test Environment' }, ], } = options; // Mock /api/system/auth-mode endpoint await page.route('**/api/system/auth-mode', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ mode: 'multi-server', supports_email_login: true, }), }); }); // Mock both /api/auth/check-email and /api/auth/check-identity const handleIdentityCheck = async (route) => { if (!emailExists) { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ exists: false, servers: [], identity_type: 'unknown', }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ exists: true, servers: servers, identity_type: 'email', }), }); }; await page.route('**/api/auth/check-email', handleIdentityCheck); await page.route('**/api/auth/check-identity', handleIdentityCheck); // Mock /api/auth/login endpoint await page.route('**/api/auth/login', async (route) => { const body = route.request().postDataJSON(); const serverId = body?.server_id || 'romfast'; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ access_token: generateMockJWT(serverId), refresh_token: generateMockJWT(serverId), token_type: 'bearer', user: MOCK_USER, }), }); }); } test('should pre-select server from URL query parameter ?server=test_server', async ({ page }) => { await setupURLPreselectionMocks(page); // Clear any previous localStorage to ensure clean state await page.addInitScript(() => { localStorage.removeItem('last_server_id'); }); // Navigate with server query parameter await page.goto('/login?server=test_server'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and trigger blur to load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Verify that 'test_server' is pre-selected (Test Environment) const selectedValue = await page.locator('#server .p-dropdown-label').textContent(); expect(selectedValue).toContain('Test Environment'); }); test('should pre-select server from URL even with different last_server_id in localStorage', async ({ page }) => { await setupURLPreselectionMocks(page); // Pre-set a different last_server_id in localStorage await page.addInitScript(() => { localStorage.setItem('last_server_id', 'romfast'); }); // Navigate with server query parameter (different from localStorage) await page.goto('/login?server=dev'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and trigger blur to load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // URL parameter should take priority over localStorage const selectedValue = await page.locator('#server .p-dropdown-label').textContent(); expect(selectedValue).toContain('Development Server'); }); test('should gracefully fallback to first server when URL specifies nonexistent server', async ({ page }) => { await setupURLPreselectionMocks(page); // Clear localStorage to ensure fallback to first server await page.addInitScript(() => { localStorage.removeItem('last_server_id'); }); // Navigate with invalid server query parameter await page.goto('/login?server=nonexistent_server_xyz'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and trigger blur to load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Should fallback to first server (Romfast - Producție) const selectedValue = await page.locator('#server .p-dropdown-label').textContent(); expect(selectedValue).toContain('Romfast'); }); test('should fallback to last_server_id when URL specifies invalid server', async ({ page }) => { await setupURLPreselectionMocks(page); // Pre-set last_server_id in localStorage await page.addInitScript(() => { localStorage.setItem('last_server_id', 'dev'); }); // Navigate with invalid server query parameter await page.goto('/login?server=invalid_server'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and trigger blur to load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // Invalid URL param should be ignored, fallback to last_server_id from localStorage const selectedValue = await page.locator('#server .p-dropdown-label').textContent(); expect(selectedValue).toContain('Development Server'); }); test('should work with URL parameter without login page prefix', async ({ page }) => { await setupURLPreselectionMocks(page); await page.addInitScript(() => { localStorage.removeItem('last_server_id'); }); // Navigate to root with server parameter (should redirect to login with param preserved) await page.goto('/?server=test_server'); // May redirect to /login, wait for identity field await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and trigger blur await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); // Wait for servers to load await waitForServersLoaded(page); // The server should still be pre-selected if query param was preserved // Note: This depends on router behavior - if param is NOT preserved, it falls back to first server const selectedValue = await page.locator('#server .p-dropdown-label').textContent(); // Either test_server is selected OR romfast (first) if param wasn't preserved expect(selectedValue).toMatch(/Test Environment|Romfast/); }); test('should not pre-select server in single-server mode', async ({ page }) => { // Mock single-server mode await page.route('**/api/system/auth-mode', async (route) => { await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ mode: 'single-server', supports_email_login: false, }), }); }); // Navigate with server query parameter await page.goto('/login?server=test_server'); await page.waitForSelector('input#identity', { timeout: 10000 }); // In single-server mode, server dropdown should not be visible const serverDropdown = page.locator('#server'); await expect(serverDropdown).not.toBeVisible(); }); test('should complete login with pre-selected server from URL', async ({ page }) => { let capturedServerId = null; await setupURLPreselectionMocks(page); // Override login mock to capture server_id await page.route('**/api/auth/login', async (route) => { const body = route.request().postDataJSON(); capturedServerId = body?.server_id; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ access_token: generateMockJWT(capturedServerId), refresh_token: generateMockJWT(capturedServerId), token_type: 'bearer', user: MOCK_USER, }), }); }); await page.addInitScript(() => { localStorage.removeItem('last_server_id'); }); // Navigate with server query parameter await page.goto('/login?server=dev'); await page.waitForSelector('input#identity', { timeout: 10000 }); // Enter username and load servers await page.fill('input#identity', 'user@example.com'); await page.locator('input#identity').blur(); await waitForServersLoaded(page); // Fill password and submit await page.locator('#password input').fill('testpassword'); await page.click('button:has-text("Autentificare")'); // Wait for redirect await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 10000 }); // Verify the login request used the pre-selected server expect(capturedServerId).toBe('dev'); // Verify last_server_id was saved const lastServerId = await page.evaluate(() => localStorage.getItem('last_server_id')); expect(lastServerId).toBe('dev'); }); });