duoqi-api/src/__tests__/services/admin/admin-auth.test.ts
Wang Zhuoxuan 2c97412c82
Some checks failed
CI/CD Pipeline / Code Quality (push) Successful in 18s
CI/CD Pipeline / Unit Tests (push) Failing after 14s
CI/CD Pipeline / Build & Deploy Test (push) Has been skipped
CI/CD Pipeline / Build & Deploy Production (push) Has been skipped
fix: 修复 admin-auth 测试的 TypeScript 类型错误
将 mockDb 的类型从 Record<string, Mock> 改为显式的映射类型,
消除 CI 中 "possibly undefined" 的类型检查报错。
2026-04-23 12:55:01 +08:00

93 lines
3.1 KiB
TypeScript

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