fix: resolve lint errors in test files — group imports before vi.mock blocks
- local.strategy.spec.ts: move LocalStrategy import above vi.mock calls - media-storage.service.spec.ts: move MinioMediaStorageService import above vi.mock calls - Vitest hoists vi.mock regardless of source order, so grouping imports is safe Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,124 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { LocalStrategy } from '../strategies/local.strategy';
|
||||||
|
|
||||||
|
// Mock passport-local before importing LocalStrategy
|
||||||
|
vi.mock('passport-local', () => {
|
||||||
|
return {
|
||||||
|
Strategy: class MockStrategy {
|
||||||
|
constructor(_options: any) {}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('@nestjs/passport', () => {
|
||||||
|
return {
|
||||||
|
PassportStrategy: (StrategyClass: any) => {
|
||||||
|
return class extends StrategyClass {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('@modules/shared', () => {
|
||||||
|
class UnauthorizedException extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'UnauthorizedException';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
UnauthorizedException,
|
||||||
|
normalizeVietnamPhone: (phone: string) => {
|
||||||
|
if (phone.startsWith('+84') && phone.length === 12) return phone;
|
||||||
|
if (phone.startsWith('0') && phone.length === 10) return '+84' + phone.slice(1);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('LocalStrategy', () => {
|
||||||
|
let strategy: LocalStrategy;
|
||||||
|
let mockUserRepo: {
|
||||||
|
findByPhone: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockUserRepo = {
|
||||||
|
findByPhone: vi.fn(),
|
||||||
|
};
|
||||||
|
strategy = new LocalStrategy(mockUserRepo as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when phone number is invalid', async () => {
|
||||||
|
await expect(strategy.validate('12345', 'password')).rejects.toThrow(
|
||||||
|
'Số điện thoại không hợp lệ',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when user is not found', async () => {
|
||||||
|
mockUserRepo.findByPhone.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
|
||||||
|
'Số điện thoại hoặc mật khẩu không đúng',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when user has no password hash', async () => {
|
||||||
|
mockUserRepo.findByPhone.mockResolvedValue({
|
||||||
|
id: 'user-1',
|
||||||
|
passwordHash: null,
|
||||||
|
isActive: true,
|
||||||
|
phone: { value: '+84912345678' },
|
||||||
|
role: 'BUYER',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
|
||||||
|
'Số điện thoại hoặc mật khẩu không đúng',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when user is inactive', async () => {
|
||||||
|
mockUserRepo.findByPhone.mockResolvedValue({
|
||||||
|
id: 'user-1',
|
||||||
|
passwordHash: { compare: vi.fn().mockResolvedValue(true) },
|
||||||
|
isActive: false,
|
||||||
|
phone: { value: '+84912345678' },
|
||||||
|
role: 'BUYER',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
|
||||||
|
'Tài khoản đã bị vô hiệu hóa',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when password is wrong', async () => {
|
||||||
|
mockUserRepo.findByPhone.mockResolvedValue({
|
||||||
|
id: 'user-1',
|
||||||
|
passwordHash: { compare: vi.fn().mockResolvedValue(false) },
|
||||||
|
isActive: true,
|
||||||
|
phone: { value: '+84912345678' },
|
||||||
|
role: 'BUYER',
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(strategy.validate('0912345678', 'password')).rejects.toThrow(
|
||||||
|
'Số điện thoại hoặc mật khẩu không đúng',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns user info with valid credentials', async () => {
|
||||||
|
mockUserRepo.findByPhone.mockResolvedValue({
|
||||||
|
id: 'user-1',
|
||||||
|
passwordHash: { compare: vi.fn().mockResolvedValue(true) },
|
||||||
|
isActive: true,
|
||||||
|
phone: { value: '+84912345678' },
|
||||||
|
role: 'BUYER',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await strategy.validate('0912345678', 'P@ssw0rd!');
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 'user-1',
|
||||||
|
phone: '+84912345678',
|
||||||
|
role: 'BUYER',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { MinioMediaStorageService } from '../services/media-storage.service';
|
||||||
|
|
||||||
|
// Use vi.hoisted so variables are available when vi.mock factory runs (hoisted)
|
||||||
|
const { mockSend, mockGetSignedUrl } = vi.hoisted(() => ({
|
||||||
|
mockSend: vi.fn(),
|
||||||
|
mockGetSignedUrl: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@aws-sdk/client-s3', () => {
|
||||||
|
return {
|
||||||
|
S3Client: class MockS3Client {
|
||||||
|
send = mockSend;
|
||||||
|
constructor() {}
|
||||||
|
},
|
||||||
|
PutObjectCommand: class MockPutObjectCommand {
|
||||||
|
constructor(public input: unknown) {}
|
||||||
|
},
|
||||||
|
DeleteObjectCommand: class MockDeleteObjectCommand {
|
||||||
|
constructor(public input: unknown) {}
|
||||||
|
},
|
||||||
|
HeadBucketCommand: class MockHeadBucketCommand {
|
||||||
|
constructor(public input: unknown) {}
|
||||||
|
},
|
||||||
|
CreateBucketCommand: class MockCreateBucketCommand {
|
||||||
|
constructor(public input: unknown) {}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('@aws-sdk/s3-request-presigner', () => ({
|
||||||
|
getSignedUrl: mockGetSignedUrl,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('MinioMediaStorageService', () => {
|
||||||
|
let service: MinioMediaStorageService;
|
||||||
|
let mockLogger: {
|
||||||
|
log: ReturnType<typeof vi.fn>;
|
||||||
|
warn: ReturnType<typeof vi.fn>;
|
||||||
|
error: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Set required env variables
|
||||||
|
process.env['MINIO_ACCESS_KEY'] = 'test-access-key';
|
||||||
|
process.env['MINIO_SECRET_KEY'] = 'test-secret-key';
|
||||||
|
process.env['MINIO_ENDPOINT'] = 'localhost';
|
||||||
|
process.env['MINIO_PORT'] = '9000';
|
||||||
|
process.env['MINIO_BUCKET'] = 'test-bucket';
|
||||||
|
process.env['MINIO_USE_SSL'] = 'false';
|
||||||
|
|
||||||
|
mockLogger = {
|
||||||
|
log: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
service = new MinioMediaStorageService(mockLogger as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onModuleInit', () => {
|
||||||
|
it('should log when bucket already exists', async () => {
|
||||||
|
mockSend.mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
await service.onModuleInit();
|
||||||
|
|
||||||
|
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('test-bucket'),
|
||||||
|
'MinioMediaStorageService',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create bucket when it does not exist (404)', async () => {
|
||||||
|
const notFoundError = { $metadata: { httpStatusCode: 404 } };
|
||||||
|
mockSend.mockRejectedValueOnce(notFoundError).mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
await service.onModuleInit();
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Creating bucket'),
|
||||||
|
'MinioMediaStorageService',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should warn when bucket cannot be verified', async () => {
|
||||||
|
mockSend.mockRejectedValueOnce({ $metadata: { httpStatusCode: 500 } });
|
||||||
|
|
||||||
|
await service.onModuleInit();
|
||||||
|
|
||||||
|
expect(mockLogger.warn).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Could not verify bucket'),
|
||||||
|
'MinioMediaStorageService',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('upload', () => {
|
||||||
|
it('should upload a file and return URL', async () => {
|
||||||
|
mockSend.mockResolvedValueOnce({});
|
||||||
|
const buffer = Buffer.from('test-image-data');
|
||||||
|
|
||||||
|
const url = await service.upload(buffer, 'photo.jpg', 'image/jpeg', 'listings');
|
||||||
|
|
||||||
|
expect(url).toContain('http://localhost:9000/test-bucket/listings/');
|
||||||
|
expect(url).toContain('.jpg');
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Media uploaded'),
|
||||||
|
'MinioMediaStorageService',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw and log error when upload fails', async () => {
|
||||||
|
mockSend.mockRejectedValueOnce(new Error('Network error'));
|
||||||
|
const buffer = Buffer.from('test-data');
|
||||||
|
|
||||||
|
await expect(service.upload(buffer, 'file.png', 'image/png', 'media')).rejects.toThrow('Network error');
|
||||||
|
expect(mockLogger.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should delete a file by URL', async () => {
|
||||||
|
mockSend.mockResolvedValueOnce({});
|
||||||
|
|
||||||
|
await service.delete('http://localhost:9000/test-bucket/listings/123-abc.jpg');
|
||||||
|
|
||||||
|
expect(mockSend).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockLogger.log).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Media deleted'),
|
||||||
|
'MinioMediaStorageService',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw and log error when delete fails', async () => {
|
||||||
|
mockSend.mockRejectedValueOnce(new Error('Delete error'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.delete('http://localhost:9000/test-bucket/listings/abc.jpg'),
|
||||||
|
).rejects.toThrow('Delete error');
|
||||||
|
expect(mockLogger.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getPresignedUploadUrl', () => {
|
||||||
|
it('should return a presigned URL', async () => {
|
||||||
|
mockGetSignedUrl.mockResolvedValueOnce('https://presigned-url.example.com');
|
||||||
|
|
||||||
|
const url = await service.getPresignedUploadUrl('listings/test.jpg', 'image/jpeg');
|
||||||
|
|
||||||
|
expect(url).toBe('https://presigned-url.example.com');
|
||||||
|
expect(mockGetSignedUrl).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user