import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { Mock } from 'vitest'; // Mock bcryptjs vi.mock('bcryptjs', () => ({ compare: vi.fn(), hash: vi.fn(), })); // Build the mock DB once, then restore individual mocks in beforeEach function buildMockDb() { const db: { [K in 'select'|'from'|'where'|'limit'|'update'|'set']: Mock } = {} as typeof db; db.select = vi.fn().mockReturnValue(db); db.from = vi.fn().mockReturnValue(db); db.where = vi.fn().mockReturnValue(db); db.limit = vi.fn().mockResolvedValue([]); db.update = vi.fn().mockReturnValue(db); db.set = vi.fn().mockReturnValue(db); return db; } const mockDb = buildMockDb(); vi.mock('../../../db/client.js', () => ({ db: mockDb })); const { changePassword } = await import('../../../services/admin/admin-auth.js'); const bcrypt = await import('bcryptjs'); const mockAdmin = { id: 'admin-123', username: 'testadmin', passwordHash: '$2a$10$hashedoldpassword', role: 'admin', isActive: 1, }; describe('changePassword', () => { beforeEach(() => { // Reset call counts but keep implementations mockDb.select.mockClear().mockReturnValue(mockDb); mockDb.from.mockClear().mockReturnValue(mockDb); mockDb.where.mockClear().mockReturnValue(mockDb); mockDb.update.mockClear().mockReturnValue(mockDb); mockDb.set.mockClear().mockReturnValue(mockDb); // Per-test defaults mockDb.limit.mockResolvedValue([mockAdmin]); (bcrypt.compare as Mock).mockResolvedValue(true); (bcrypt.hash as Mock).mockResolvedValue('$2a$10$hashednewpassword'); }); it('changes password when current password is correct', async () => { await changePassword('admin-123', 'oldPassword1!', 'newPassword1!'); expect(bcrypt.compare).toHaveBeenCalledWith('oldPassword1!', mockAdmin.passwordHash); expect(bcrypt.hash).toHaveBeenCalledWith('newPassword1!', 10); expect(mockDb.update).toHaveBeenCalled(); expect(mockDb.set).toHaveBeenCalledWith( expect.objectContaining({ passwordHash: '$2a$10$hashednewpassword' }), ); }); it('throws UnauthorizedError when admin not found', async () => { mockDb.limit.mockResolvedValue([]); await expect(changePassword('admin-123', 'old', 'new')) .rejects.toThrow('Admin user not found'); }); it('throws ForbiddenError when admin is disabled', async () => { mockDb.limit.mockResolvedValue([{ ...mockAdmin, isActive: 0 }]); await expect(changePassword('admin-123', 'old', 'new')) .rejects.toThrow('Admin account is disabled'); }); it('throws UnauthorizedError when current password is wrong', async () => { (bcrypt.compare as Mock).mockResolvedValue(false); await expect(changePassword('admin-123', 'wrong', 'new')) .rejects.toThrow('Current password is incorrect'); expect(mockDb.update).not.toHaveBeenCalled(); }); it('throws ValidationError when new password equals current password', async () => { await expect(changePassword('admin-123', 'samePass1!', 'samePass1!')) .rejects.toThrow('New password must be different from current password'); expect(mockDb.update).not.toHaveBeenCalled(); }); });