189 lines
7.3 KiB
TypeScript
189 lines
7.3 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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());
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|