feat: multi-Oracle server support with runtime switching
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>
This commit is contained in:
@@ -5,7 +5,7 @@ import path from 'path';
|
||||
* E2E Tests for Bulk Receipt Upload (US-005)
|
||||
*
|
||||
* Prerequisites:
|
||||
* 1. Start the test environment: ./start-test.sh
|
||||
* 1. Start the test environment: ./start.sh test
|
||||
* 2. Ensure backend is running on port 8000
|
||||
* 3. Ensure frontend is running on port 3000
|
||||
*
|
||||
|
||||
1413
e2e/multi-server-login.spec.js
Normal file
1413
e2e/multi-server-login.spec.js
Normal file
File diff suppressed because it is too large
Load Diff
209
e2e/single-server-login.spec.js
Normal file
209
e2e/single-server-login.spec.js
Normal file
@@ -0,0 +1,209 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user