/** * 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()); }); }); });