feat: implement ERP authentication service
Implement ErpAuthService with TDD approach: - Add login(), close(), getSession(), isActive() methods - Use Playwright chromium with headless:false for debugging - Manage browser lifecycle and session state - Handle authentication flow with ERP_LOCATORS - Add integration tests for login scenarios - Add unit tests for session management - Fix dotenv config path in test setup Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
93
src/main/services/erp/erp-auth.ts
Normal file
93
src/main/services/erp/erp-auth.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { chromium, type Browser, type BrowserContext, type Page } from 'playwright';
|
||||||
|
import { ERP_LOCATORS } from './locators';
|
||||||
|
import type { ErpConfig, ErpSession } from '../../types/erp.types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ERP Authentication Service
|
||||||
|
* Manages login session and browser lifecycle
|
||||||
|
*/
|
||||||
|
export class ErpAuthService {
|
||||||
|
private config: ErpConfig;
|
||||||
|
private session: ErpSession | null = null;
|
||||||
|
|
||||||
|
constructor(config: ErpConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Login to ERP system and establish session
|
||||||
|
*/
|
||||||
|
async login(): Promise<ErpSession> {
|
||||||
|
if (this.session?.isLoggedIn) {
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch browser
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
headless: false, // Set to true for production
|
||||||
|
slowMo: 100, // Slow down for debugging
|
||||||
|
});
|
||||||
|
|
||||||
|
const context = await browser.newContext({
|
||||||
|
acceptDownloads: true,
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
// Navigate to login page
|
||||||
|
await page.goto(this.config.url);
|
||||||
|
|
||||||
|
// Wait for login form
|
||||||
|
await page.waitForSelector(ERP_LOCATORS.login.usernameInput);
|
||||||
|
|
||||||
|
// Fill credentials
|
||||||
|
await page.fill(ERP_LOCATORS.login.usernameInput, this.config.username);
|
||||||
|
await page.fill(ERP_LOCATORS.login.passwordInput, this.config.password);
|
||||||
|
|
||||||
|
// Submit login
|
||||||
|
await page.click(ERP_LOCATORS.login.submitButton);
|
||||||
|
|
||||||
|
// Wait for main page to load
|
||||||
|
await page.waitForURL(`${this.config.url}/**`);
|
||||||
|
await page.waitForSelector(ERP_LOCATORS.main.mainIframe, { timeout: 10000 });
|
||||||
|
|
||||||
|
// Create session
|
||||||
|
this.session = {
|
||||||
|
browser,
|
||||||
|
context,
|
||||||
|
page,
|
||||||
|
isLoggedIn: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close browser and cleanup session
|
||||||
|
*/
|
||||||
|
async close(): Promise<void> {
|
||||||
|
if (this.session) {
|
||||||
|
await this.session.context.close();
|
||||||
|
await this.session.browser.close();
|
||||||
|
this.session = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current session (must be logged in first)
|
||||||
|
*/
|
||||||
|
getSession(): ErpSession {
|
||||||
|
if (!this.session?.isLoggedIn) {
|
||||||
|
throw new Error('Not logged in. Call login() first.');
|
||||||
|
}
|
||||||
|
return this.session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if session is active
|
||||||
|
*/
|
||||||
|
isActive(): boolean {
|
||||||
|
return this.session?.isLoggedIn ?? false;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
tests/integration/erp-auth.test.ts
Normal file
40
tests/integration/erp-auth.test.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
|
import { ErpAuthService } from '../../src/main/services/erp/erp-auth';
|
||||||
|
import type { ErpConfig } from '../../src/main/types/erp.types';
|
||||||
|
|
||||||
|
describe('ERP Authentication Service (Integration)', () => {
|
||||||
|
let authService: ErpAuthService;
|
||||||
|
const config: ErpConfig = {
|
||||||
|
url: process.env.ERP_URL || '',
|
||||||
|
username: process.env.ERP_USERNAME || '',
|
||||||
|
password: process.env.ERP_PASSWORD || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
if (!config.url || !config.username || !config.password) {
|
||||||
|
throw new Error('Missing ERP credentials in environment variables');
|
||||||
|
}
|
||||||
|
authService = new ErpAuthService(config);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should login successfully', async () => {
|
||||||
|
const session = await authService.login();
|
||||||
|
|
||||||
|
expect(session).toBeDefined();
|
||||||
|
expect(session.browser).toBeDefined();
|
||||||
|
expect(session.context).toBeDefined();
|
||||||
|
expect(session.page).toBeDefined();
|
||||||
|
expect(session.isLoggedIn).toBe(true);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
it('should navigate to main page after login', async () => {
|
||||||
|
const session = await authService.login();
|
||||||
|
|
||||||
|
const url = session.page.url();
|
||||||
|
expect(url).toContain(config.url);
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await authService?.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { beforeAll, afterAll } from 'vitest';
|
import { beforeAll, afterAll } from 'vitest';
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables from project root
|
||||||
dotenv.config({ path: '.env' });
|
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
// Global test setup
|
// Global test setup
|
||||||
|
|||||||
59
tests/unit/erp-auth.unit.test.ts
Normal file
59
tests/unit/erp-auth.unit.test.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import { ErpAuthService } from '../../src/main/services/erp/erp-auth';
|
||||||
|
import type { ErpConfig } from '../../src/main/types/erp.types';
|
||||||
|
|
||||||
|
describe('ERP Authentication Service (Unit)', () => {
|
||||||
|
describe('Session Management', () => {
|
||||||
|
it('should create service instance with config', () => {
|
||||||
|
const config: ErpConfig = {
|
||||||
|
url: 'https://test.example.com',
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'testpass',
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new ErpAuthService(config);
|
||||||
|
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
expect(service.isActive()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw error when getting session before login', () => {
|
||||||
|
const config: ErpConfig = {
|
||||||
|
url: 'https://test.example.com',
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'testpass',
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new ErpAuthService(config);
|
||||||
|
|
||||||
|
expect(() => service.getSession()).toThrow('Not logged in. Call login() first.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should report inactive status before login', () => {
|
||||||
|
const config: ErpConfig = {
|
||||||
|
url: 'https://test.example.com',
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'testpass',
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new ErpAuthService(config);
|
||||||
|
|
||||||
|
expect(service.isActive()).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Close Method', () => {
|
||||||
|
it('should handle close when no session exists', async () => {
|
||||||
|
const config: ErpConfig = {
|
||||||
|
url: 'https://test.example.com',
|
||||||
|
username: 'testuser',
|
||||||
|
password: 'testpass',
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new ErpAuthService(config);
|
||||||
|
|
||||||
|
// Should not throw when closing without session
|
||||||
|
await expect(service.close()).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user