Complete implementation of multi-server Oracle database support: Backend: - Multi-pool Oracle with lazy loading per server - Email-to-server cache for automatic server discovery - JWT tokens include server_id claim - /auth/check-identity and /auth/check-email endpoints - /auth/my-servers endpoint for listing user's accessible servers - Server switch with password re-authentication Frontend: - New ServerSelector component for header dropdown - Multi-step login flow (identity → server → password) - Server switching from header with password modal - Mobile drawer menu with server selection - Dark mode support for all new components - URL bookmark support with ?server= query param Scripts: - Unified start.sh replacing start-prod.sh/start-test.sh - Unified ssh-tunnel.sh with multi-server support - Updated status.sh for new architecture Tests: - E2E tests for multi-server and single-server login flows - Backend unit tests for all new endpoints - Oracle multi-pool integration tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
210 lines
7.4 KiB
JavaScript
210 lines
7.4 KiB
JavaScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* E2E Tests for Backward Compatibility - Single-Server Login (US-011)
|
|
*
|
|
* These tests verify that the classic username/password login flow works
|
|
* when ORACLE_SERVERS is NOT configured (single-server mode).
|
|
*
|
|
* Prerequisites:
|
|
* 1. Backend running WITHOUT ORACLE_SERVERS env variable
|
|
* 2. Start test environment: ./start.sh test
|
|
* 3. Frontend running on port 3000
|
|
*
|
|
* Run:
|
|
* npm run test:e2e -- single-server-login.spec.js
|
|
* npm run test:e2e:headed -- single-server-login.spec.js
|
|
*/
|
|
|
|
// Test configuration for single-server mode
|
|
const TEST_USER = {
|
|
username: 'MARIUS M',
|
|
password: '123',
|
|
company: 'MARIUSM AUTO'
|
|
};
|
|
|
|
test.describe('Single-Server Login Backward Compatibility (US-011)', () => {
|
|
|
|
test('should show username/password form in single-server mode', async ({ page }) => {
|
|
// Navigate to login page
|
|
await page.goto('/');
|
|
|
|
// Wait for auth mode detection to complete
|
|
// The login form should show username field (not email) in single-server mode
|
|
await page.waitForSelector('input#identity', { timeout: 10000 });
|
|
|
|
// Verify username field is visible (single-server mode)
|
|
const usernameField = page.locator('input#identity');
|
|
const emailField = page.locator('input#identity');
|
|
|
|
// In single-server mode, username field should be visible
|
|
// In multi-server mode, email field would be visible instead
|
|
const isUsernameVisible = await usernameField.isVisible().catch(() => false);
|
|
const isEmailVisible = await emailField.isVisible().catch(() => false);
|
|
|
|
// At least one should be visible
|
|
expect(isUsernameVisible || isEmailVisible).toBe(true);
|
|
|
|
if (isUsernameVisible) {
|
|
// Single-server mode - verify password field is also present
|
|
const passwordField = page.locator('#password input');
|
|
await expect(passwordField).toBeVisible();
|
|
|
|
// Verify "Autentificare" button exists (not "Continuă")
|
|
await expect(page.locator('button:has-text("Autentificare")')).toBeVisible();
|
|
}
|
|
});
|
|
|
|
test('should successfully login with username/password', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Wait for form to load
|
|
await page.waitForSelector('input#identity', { timeout: 10000 });
|
|
|
|
// Check which mode we're in
|
|
const usernameField = page.locator('input#identity');
|
|
const isUsernameVisible = await usernameField.isVisible().catch(() => false);
|
|
|
|
if (isUsernameVisible) {
|
|
// Single-server mode: fill username and password
|
|
await page.fill('input#identity', TEST_USER.username);
|
|
await page.fill('#password input', TEST_USER.password);
|
|
|
|
// Click login button
|
|
await page.click('button:has-text("Autentificare")');
|
|
|
|
// Wait for redirect after successful login
|
|
await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 15000 });
|
|
|
|
// Verify we're logged in (check for logout button or user menu)
|
|
const logoutButton = page.locator('button:has-text("Deconectare"), [class*="logout"]');
|
|
const userMenu = page.locator('[class*="user"], [class*="profile"]');
|
|
|
|
// Should be redirected away from login page
|
|
expect(page.url()).not.toContain('/login');
|
|
} else {
|
|
// Multi-server mode: use email flow (existing tests cover this)
|
|
console.log('Multi-server mode detected - skipping single-server login test');
|
|
}
|
|
});
|
|
|
|
test('should show error for invalid credentials', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
// Wait for form to load
|
|
await page.waitForSelector('input#identity', { timeout: 10000 });
|
|
|
|
const usernameField = page.locator('input#identity');
|
|
const isUsernameVisible = await usernameField.isVisible().catch(() => false);
|
|
|
|
if (isUsernameVisible) {
|
|
// Single-server mode: test invalid credentials
|
|
await page.fill('input#identity', 'INVALID_USER');
|
|
await page.fill('#password input', 'wrong_password');
|
|
|
|
// Click login button
|
|
await page.click('button:has-text("Autentificare")');
|
|
|
|
// Wait for error message
|
|
await page.waitForTimeout(2000); // Wait for API response
|
|
|
|
// Check for error toast or message
|
|
const errorToast = page.locator('.p-toast-message-error, .p-toast-error, [class*="error"]');
|
|
const errorMessage = page.locator('[class*="error-message"], .p-message-error');
|
|
|
|
// Should show some form of error
|
|
const hasError = await errorToast.isVisible().catch(() => false) ||
|
|
await errorMessage.isVisible().catch(() => false);
|
|
|
|
// Should still be on login page (not redirected)
|
|
expect(page.url()).toMatch(/\/$|\/login/);
|
|
}
|
|
});
|
|
|
|
test('should preserve JWT in localStorage after login', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
await page.waitForSelector('input#identity', { timeout: 10000 });
|
|
|
|
const usernameField = page.locator('input#identity');
|
|
const isUsernameVisible = await usernameField.isVisible().catch(() => false);
|
|
|
|
if (isUsernameVisible) {
|
|
await page.fill('input#identity', TEST_USER.username);
|
|
await page.fill('#password input', TEST_USER.password);
|
|
await page.click('button:has-text("Autentificare")');
|
|
|
|
await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 15000 });
|
|
|
|
// Check localStorage for JWT
|
|
const accessToken = await page.evaluate(() => localStorage.getItem('access_token'));
|
|
const user = await page.evaluate(() => localStorage.getItem('user'));
|
|
|
|
// JWT should be stored
|
|
expect(accessToken).toBeTruthy();
|
|
expect(accessToken.split('.').length).toBe(3); // Valid JWT format
|
|
|
|
// User should be stored
|
|
expect(user).toBeTruthy();
|
|
const userData = JSON.parse(user);
|
|
expect(userData).toHaveProperty('username');
|
|
}
|
|
});
|
|
|
|
test('should access reports after login', async ({ page }) => {
|
|
await page.goto('/');
|
|
|
|
await page.waitForSelector('input#identity', { timeout: 10000 });
|
|
|
|
const usernameField = page.locator('input#identity');
|
|
const isUsernameVisible = await usernameField.isVisible().catch(() => false);
|
|
|
|
if (isUsernameVisible) {
|
|
// Login first
|
|
await page.fill('input#identity', TEST_USER.username);
|
|
await page.fill('#password input', TEST_USER.password);
|
|
await page.click('button:has-text("Autentificare")');
|
|
|
|
await page.waitForURL(/\/(reports|data-entry|dashboard)/, { timeout: 15000 });
|
|
|
|
// Navigate to reports if not already there
|
|
await page.goto('/reports');
|
|
|
|
// Wait for page to load
|
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
|
|
// Should not be redirected back to login
|
|
await page.waitForTimeout(2000);
|
|
expect(page.url()).not.toMatch(/\/$|\/login/);
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
test.describe('Auth Mode Detection', () => {
|
|
|
|
test('should return valid auth-mode response', async ({ request }) => {
|
|
// Test the auth-mode endpoint directly
|
|
const response = await request.get('/api/system/auth-mode');
|
|
|
|
expect(response.ok()).toBe(true);
|
|
|
|
const data = await response.json();
|
|
|
|
// Should have required fields
|
|
expect(data).toHaveProperty('mode');
|
|
expect(data).toHaveProperty('supports_email_login');
|
|
|
|
// Mode should be either single-server or multi-server
|
|
expect(['single-server', 'multi-server']).toContain(data.mode);
|
|
|
|
// supports_email_login should match mode
|
|
if (data.mode === 'single-server') {
|
|
expect(data.supports_email_login).toBe(false);
|
|
} else {
|
|
expect(data.supports_email_login).toBe(true);
|
|
}
|
|
});
|
|
|
|
});
|