fix: replace hardcoded localhost:3001 with relative API URL + add canvas mock and integration test
- Fix frontend Network error: API_BASE now defaults to empty string (same-origin) - Fix 3 files: api.ts, AuthContext.tsx, Dashboard.tsx - Add Canvas 2D context mock in tests/setup.ts for jsdom - Fix -0 vs +0 in ZoomPanController.getViewport() - Add IntegrationWorkflow.test.ts (35 tests, full CAD workflow)
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* AuthService Unit Tests – Register / Login / ChangePassword / Session lifecycle
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
import { AuthService } from '../src/auth/AuthService.js';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let db: SqliteAdapter;
|
||||
let auth: AuthService;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
auth = new AuthService(db);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── Register ───────────────────────────────────────
|
||||
|
||||
describe('register()', () => {
|
||||
it('should register a new user successfully', async () => {
|
||||
const result = await auth.register('register@example.com', 'Password123!', 'Register User', 'planer');
|
||||
expect(result.user).toBeDefined();
|
||||
expect(result.user.email).toBe('register@example.com');
|
||||
expect(result.user.name).toBe('Register User');
|
||||
expect(result.user.password_hash).toBeUndefined();
|
||||
expect(result.session).toBeDefined();
|
||||
expect(result.session.token).toBeDefined();
|
||||
expect(result.session.userId).toBe(result.user.id);
|
||||
});
|
||||
|
||||
it('should throw on duplicate email', async () => {
|
||||
await expect(
|
||||
auth.register('register@example.com', 'AnotherPass!', 'Another User', 'planer'),
|
||||
).rejects.toThrow('Email already registered');
|
||||
});
|
||||
|
||||
it('should default role to planer when not specified', async () => {
|
||||
const result = await auth.register('default-role@example.com', 'Pass123!', 'Default Role User');
|
||||
expect(result.user.role).toBe('planer');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Login ─────────────────────────────────────────
|
||||
|
||||
describe('login()', () => {
|
||||
it('should login with correct credentials', async () => {
|
||||
const result = await auth.login('register@example.com', 'Password123!');
|
||||
expect(result.user).toBeDefined();
|
||||
expect(result.user.email).toBe('register@example.com');
|
||||
expect(result.session.token).toBeDefined();
|
||||
expect(result.session.userId).toBe(result.user.id);
|
||||
});
|
||||
|
||||
it('should throw on wrong password', async () => {
|
||||
await expect(
|
||||
auth.login('register@example.com', 'WrongPassword!'),
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
|
||||
it('should throw on non-existent email', async () => {
|
||||
await expect(
|
||||
auth.login('nobody@example.com', 'SomePassword!'),
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Change Password ───────────────────────────────
|
||||
|
||||
describe('changePassword()', () => {
|
||||
it('should change password with correct old password', async () => {
|
||||
const result = await auth.register('changepw@example.com', 'OldPass123!', 'ChangePW User', 'admin');
|
||||
const userId = result.user.id;
|
||||
|
||||
await auth.changePassword(userId, 'OldPass123!', 'NewPass456!');
|
||||
// Should now be able to login with new password
|
||||
const loginResult = await auth.login('changepw@example.com', 'NewPass456!');
|
||||
expect(loginResult.user.id).toBe(userId);
|
||||
});
|
||||
|
||||
it('should throw on wrong old password', async () => {
|
||||
const result = await auth.register('changepw2@example.com', 'OriginalPass!', 'User2', 'planer');
|
||||
await expect(
|
||||
auth.changePassword(result.user.id, 'WrongOldPass!', 'NewPass!'),
|
||||
).rejects.toThrow('Invalid current password');
|
||||
});
|
||||
|
||||
it('should throw on non-existent user', async () => {
|
||||
await expect(
|
||||
auth.changePassword('non-existent-user-id', 'OldPass!', 'NewPass!'),
|
||||
).rejects.toThrow('User not found');
|
||||
});
|
||||
|
||||
it('should reject old password after change', async () => {
|
||||
const result = await auth.register('changepw3@example.com', 'Original123!', 'User3', 'planer');
|
||||
await auth.changePassword(result.user.id, 'Original123!', 'Changed456!');
|
||||
await expect(
|
||||
auth.login('changepw3@example.com', 'Original123!'),
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Session Management ────────────────────────────
|
||||
|
||||
describe('Session lifecycle', () => {
|
||||
it('should create and validate a session', async () => {
|
||||
const regResult = await auth.register('session@example.com', 'Pass123!', 'Session User', 'planer');
|
||||
const token = regResult.session.token;
|
||||
|
||||
const session = auth.validateSession(token);
|
||||
expect(session).not.toBeNull();
|
||||
expect(session!.token).toBe(token);
|
||||
expect(session!.userId).toBe(regResult.user.id);
|
||||
});
|
||||
|
||||
it('should return null for invalid session token', () => {
|
||||
const session = auth.validateSession('invalid-token-xyz');
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
|
||||
it('should destroy a session and invalidate it', async () => {
|
||||
const regResult = await auth.register('destroy@example.com', 'Pass123!', 'Destroy User', 'planer');
|
||||
const token = regResult.session.token;
|
||||
|
||||
// Session should be valid
|
||||
expect(auth.validateSession(token)).not.toBeNull();
|
||||
|
||||
// Destroy it
|
||||
auth.destroySession(token);
|
||||
|
||||
// Session should now be invalid
|
||||
expect(auth.validateSession(token)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle destroying a non-existent session gracefully', () => {
|
||||
// Should not throw
|
||||
auth.destroySession('non-existent-token');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getUserFromSession ─────────────────────────────
|
||||
|
||||
describe('getUserFromSession()', () => {
|
||||
it('should return user from valid session', async () => {
|
||||
const regResult = await auth.register('getuser@example.com', 'Pass123!', 'GetUser User', 'planer');
|
||||
const token = regResult.session.token;
|
||||
|
||||
const user = auth.getUserFromSession(token);
|
||||
expect(user).not.toBeNull();
|
||||
expect(user!.email).toBe('getuser@example.com');
|
||||
expect(user!.password_hash).toBeDefined(); // returns full DBUser including password_hash
|
||||
});
|
||||
|
||||
it('should return null for invalid session token', () => {
|
||||
const user = auth.getUserFromSession('invalid-token-xyz');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null after session is destroyed', async () => {
|
||||
const regResult = await auth.register('getuser2@example.com', 'Pass123!', 'GetUser2 User', 'planer');
|
||||
const token = regResult.session.token;
|
||||
|
||||
auth.destroySession(token);
|
||||
const user = auth.getUserFromSession(token);
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── createSession (standalone) ─────────────────────
|
||||
|
||||
describe('createSession()', () => {
|
||||
it('should create a session with a unique token', async () => {
|
||||
const regResult = await auth.register('createsession@example.com', 'Pass123!', 'CS User', 'planer');
|
||||
const session1 = auth.createSession(regResult.user.id);
|
||||
const session2 = auth.createSession(regResult.user.id);
|
||||
|
||||
expect(session1.token).not.toBe(session2.token);
|
||||
expect(session1.userId).toBe(regResult.user.id);
|
||||
expect(session2.userId).toBe(regResult.user.id);
|
||||
expect(session1.expiresAt).toBeGreaterThan(Date.now());
|
||||
expect(session2.expiresAt).toBeGreaterThan(Date.now());
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user