Clean working version: deployed TypeScript backend + JSX frontend
- Replace old JS backend with working TypeScript backend from deployed containers - Backend: Fastify + better-sqlite3 + Yjs CRDT, 17 TS source files, 13 test files - Frontend: React JSX with fabric.js CAD canvas, vite build, nginx serving - Fix nginx proxy port 5000→3001 to match backend - Fix docker-compose port mapping 5000→3001 - Fix vite dev proxy port 5000→3001 - Add backend healthcheck to docker-compose - Update index.html title to 'Web CAD', lang to 'de' - Add .a0/ to .gitignore - Remove old JS backend files (models, routes, config, middleware, seed) - Remove tracked build artifacts (backend/public/)
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