feat: add more roles (tech_lead, scrum_master, product_owner, designer, qa)

- Registration form: added 5 new role options to dropdown
- Sidebar: new roles get proper nav access via ALL_ROLES/LEADER_ROLES
- Dashboard: isLeader check expanded to include new leadership roles
- Shared/Pages: role badge colors added for all new roles
- Invite modal: expanded role dropdown
This commit is contained in:
tusuii
2026-02-16 12:31:54 +05:30
parent 2db45de4c4
commit c604df281d
33 changed files with 5006 additions and 71 deletions

127
server/tests/auth.test.js Normal file
View File

@@ -0,0 +1,127 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import { app } from '../index.js';
import pool from '../db.js';
import bcrypt from 'bcryptjs';
// Mock dependencies
vi.mock('../db.js', () => ({
default: {
query: vi.fn(),
},
initDB: vi.fn()
}));
vi.mock('bcryptjs', () => ({
default: {
compare: vi.fn(),
hash: vi.fn()
}
}));
describe('Auth Routes', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('POST /api/auth/login', () => {
it('logs in successfully with correct credentials', async () => {
const mockUser = {
id: 'u1',
name: 'Test User',
email: 'test@test.com',
password_hash: 'hashed_password',
role: 'employee',
dept: 'dev'
};
// Mock DB response
pool.query.mockResolvedValue([[mockUser]]);
// Mock bcrypt comparison
bcrypt.compare.mockResolvedValue(true);
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'test@test.com', password: 'password' });
expect(res.status).toBe(200);
expect(res.body).toEqual(expect.objectContaining({
id: 'u1',
email: 'test@test.com'
}));
expect(res.body).not.toHaveProperty('password_hash');
});
it('returns 401 for invalid password', async () => {
const mockUser = {
id: 'u1',
email: 'test@test.com',
password_hash: 'hashed_password'
};
pool.query.mockResolvedValue([[mockUser]]);
bcrypt.compare.mockResolvedValue(false);
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'test@test.com', password: 'wrong' });
expect(res.status).toBe(401);
expect(res.body).toEqual({ error: 'Invalid email or password' });
});
it('returns 401 for user not found', async () => {
pool.query.mockResolvedValue([[]]); // Empty array
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'notfound@test.com', password: 'password' });
expect(res.status).toBe(401);
});
});
describe('POST /api/auth/register', () => {
it('registers a new user successfully', async () => {
pool.query.mockResolvedValueOnce([[]]); // Check existing email (empty)
pool.query.mockResolvedValueOnce({}); // Insert success
bcrypt.hash.mockResolvedValue('hashed_new_password');
const newUser = {
name: 'New User',
email: 'new@test.com',
password: 'password',
role: 'employee'
};
const res = await request(app)
.post('/api/auth/register')
.send(newUser);
expect(res.status).toBe(201);
expect(res.body).toEqual(expect.objectContaining({
name: 'New User',
email: 'new@test.com'
}));
// Verify DB insert called
expect(pool.query).toHaveBeenCalledTimes(2);
expect(pool.query).toHaveBeenLastCalledWith(
expect.stringContaining('INSERT INTO users'),
expect.arrayContaining(['New User', 'employee', 'new@test.com', 'hashed_new_password'])
);
});
it('returns 409 if email already exists', async () => {
pool.query.mockResolvedValueOnce([[{ id: 'existing' }]]);
const res = await request(app)
.post('/api/auth/register')
.send({ name: 'User', email: 'existing@test.com', password: 'pw' });
expect(res.status).toBe(409);
});
});
});

120
server/tests/tasks.test.js Normal file
View File

@@ -0,0 +1,120 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import request from 'supertest';
import { app } from '../index.js';
import pool from '../db.js';
// Mock DB
const mockQuery = vi.fn();
const mockRelease = vi.fn();
const mockCommit = vi.fn();
const mockRollback = vi.fn();
const mockBeginTransaction = vi.fn();
vi.mock('../db.js', () => ({
default: {
query: vi.fn(),
getConnection: vi.fn()
},
initDB: vi.fn()
}));
describe('Task Routes', () => {
beforeEach(() => {
vi.clearAllMocks();
mockQuery.mockReset(); // Important to clear implementations
pool.query = mockQuery;
pool.getConnection.mockResolvedValue({
query: mockQuery,
release: mockRelease,
beginTransaction: mockBeginTransaction,
commit: mockCommit,
rollback: mockRollback
});
});
describe('GET /api/tasks', () => {
it('returns list of tasks', async () => {
// Mock fetching task IDs
mockQuery.mockResolvedValueOnce([[{ id: 't1' }]]);
// Mock getFullTask queries for 't1'
// 1. Task details
mockQuery.mockResolvedValueOnce([[{ id: 't1', title: 'Task 1', status: 'todo' }]]); // Task
// 2. Subtasks
mockQuery.mockResolvedValueOnce([[]]); // Subtasks
// 3. Comments
mockQuery.mockResolvedValueOnce([[]]); // Comments
// 4. Activities
mockQuery.mockResolvedValueOnce([[]]); // Activities
// 5. Tags
mockQuery.mockResolvedValueOnce([[]]); // Tags
// 6. Dependencies
mockQuery.mockResolvedValueOnce([[]]); // Dependencies
const res = await request(app).get('/api/tasks');
expect(res.status).toBe(200);
expect(res.body).toHaveLength(1);
expect(res.body[0].title).toBe('Task 1');
});
});
describe('POST /api/tasks', () => {
it('creates a new task', async () => {
// Mock INSERTs (1: Task, 2: Activities) -> Return {}
mockQuery.mockResolvedValueOnce({}); // Insert task
mockQuery.mockResolvedValueOnce({}); // Insert activity
// getFullTask queries (3-8)
mockQuery.mockResolvedValueOnce([[{ id: 'new-id', title: 'New Task', status: 'todo' }]]); // Task
mockQuery.mockResolvedValueOnce([[]]); // Subtasks
mockQuery.mockResolvedValueOnce([[]]); // Comments
mockQuery.mockResolvedValueOnce([[]]); // Activities
mockQuery.mockResolvedValueOnce([[]]); // Tags
mockQuery.mockResolvedValueOnce([[]]); // Deps
const newTask = {
// For getFullTask called at end
title: 'New Task',
description: 'Desc',
status: 'todo'
};
const res = await request(app)
.post('/api/tasks')
.send(newTask);
expect(res.status).toBe(201);
expect(mockBeginTransaction).toHaveBeenCalled();
expect(mockCommit).toHaveBeenCalled();
expect(mockRelease).toHaveBeenCalled();
});
it('rolls back on error', async () => {
mockQuery.mockRejectedValue(new Error('DB Error'));
const res = await request(app)
.post('/api/tasks')
.send({ title: 'Task' });
expect(res.status).toBe(500);
expect(mockRollback).toHaveBeenCalled();
expect(mockRelease).toHaveBeenCalled();
});
});
describe('DELETE /api/tasks/:id', () => {
it('deletes a task', async () => {
mockQuery.mockResolvedValue({});
const res = await request(app).delete('/api/tasks/t1');
expect(res.status).toBe(200);
expect(mockQuery).toHaveBeenCalledWith(
expect.stringContaining('DELETE FROM tasks'),
['t1']
);
});
});
});