251209:0000 Backend Test stagenot finish & Frontend add Task 013-015
Some checks failed
Spec Validation / validate-markdown (push) Has been cancelled
Spec Validation / validate-diagrams (push) Has been cancelled
Spec Validation / check-todos (push) Has been cancelled

This commit is contained in:
2025-12-09 00:00:28 +07:00
parent 863a727756
commit 8aceced902
23 changed files with 3571 additions and 118 deletions

View File

@@ -102,7 +102,16 @@ export class AuthController {
@HttpCode(HttpStatus.OK)
@ApiBearerAuth()
@ApiOperation({ summary: 'Logout (Revoke Tokens)' })
@ApiResponse({ status: 200, description: 'Logged out successfully' })
@ApiResponse({
status: 200,
description: 'Logged out successfully',
schema: {
type: 'object',
properties: {
message: { type: 'string', example: 'Logged out successfully' },
},
},
})
async logout(@Req() req: RequestWithUser) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {

View File

@@ -1,18 +1,202 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
import { UserService } from '../../modules/user/user.service';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from '../../modules/user/entities/user.entity';
import { RefreshToken } from './entities/refresh-token.entity';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { UnauthorizedException } from '@nestjs/common';
describe('AuthService', () => {
let service: AuthService;
let userService: UserService;
let jwtService: JwtService;
let tokenRepo: Repository<RefreshToken>;
const mockUser = {
user_id: 1,
username: 'testuser',
password: 'hashedpassword',
primaryOrganizationId: 1,
};
const mockQueryBuilder = {
addSelect: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
getOne: jest.fn().mockResolvedValue(mockUser),
};
const mockUserRepo = {
createQueryBuilder: jest.fn().mockReturnValue(mockQueryBuilder),
};
const mockTokenRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
providers: [
AuthService,
{
provide: UserService,
useValue: {
findOneByUsername: jest.fn(),
create: jest.fn(),
findOne: jest.fn(),
},
},
{
provide: JwtService,
useValue: {
signAsync: jest.fn().mockResolvedValue('jwt_token'),
decode: jest.fn(),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn().mockImplementation((key) => {
if (key.includes('EXPIRATION')) return '1h';
return 'secret';
}),
},
},
{
provide: CACHE_MANAGER,
useValue: {
set: jest.fn(),
},
},
{
provide: getRepositoryToken(User),
useValue: mockUserRepo,
},
{
provide: getRepositoryToken(RefreshToken),
useValue: mockTokenRepo,
},
],
}).compile();
service = module.get<AuthService>(AuthService);
userService = module.get<UserService>(UserService);
jwtService = module.get<JwtService>(JwtService);
tokenRepo = module.get(getRepositoryToken(RefreshToken));
// Mock bcrypt
jest
.spyOn(bcrypt, 'compare')
.mockImplementation(() => Promise.resolve(true));
jest
.spyOn(bcrypt, 'hash')
.mockImplementation(() => Promise.resolve('hashedpassword'));
jest
.spyOn(bcrypt, 'genSalt')
.mockImplementation(() => Promise.resolve('salt'));
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('validateUser', () => {
it('should return user without password if validation succeeds', async () => {
const result = await service.validateUser('testuser', 'password');
expect(result).toBeDefined();
expect(result).not.toHaveProperty('password');
expect(result.username).toBe('testuser');
});
it('should return null if user not found', async () => {
mockQueryBuilder.getOne.mockResolvedValueOnce(null);
const result = await service.validateUser('unknown', 'password');
expect(result).toBeNull();
});
it('should return null if password mismatch', async () => {
jest
.spyOn(bcrypt, 'compare')
.mockImplementation(() => Promise.resolve(false));
const result = await service.validateUser('testuser', 'wrongpassword');
expect(result).toBeNull();
});
});
describe('login', () => {
it('should return access and refresh tokens', async () => {
mockTokenRepo.create.mockReturnValue({ id: 1 });
mockTokenRepo.save.mockResolvedValue({ id: 1 });
const result = await service.login(mockUser);
expect(result).toHaveProperty('access_token');
expect(result).toHaveProperty('refresh_token');
expect(mockTokenRepo.save).toHaveBeenCalled();
});
});
describe('register', () => {
it('should register a new user', async () => {
(userService.findOneByUsername as jest.Mock).mockResolvedValue(null);
(userService.create as jest.Mock).mockResolvedValue(mockUser);
const dto = {
username: 'newuser',
password: 'password',
email: 'test@example.com',
firstName: 'Test',
lastName: 'User',
};
const result = await service.register(dto);
expect(result).toBeDefined();
expect(userService.create).toHaveBeenCalled();
});
});
describe('refreshToken', () => {
it('should return new tokens if valid', async () => {
const mockStoredToken = {
tokenHash: 'somehash',
isRevoked: false,
expiresAt: new Date(Date.now() + 10000),
};
mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);
(userService.findOne as jest.Mock).mockResolvedValue(mockUser);
const result = await service.refreshToken(1, 'valid_refresh_token');
expect(result.access_token).toBeDefined();
expect(result.refresh_token).toBeDefined();
// Should mark old token as revoked
expect(mockTokenRepo.save).toHaveBeenCalledWith(
expect.objectContaining({ isRevoked: true })
);
});
it('should throw UnauthorizedException if token revoked', async () => {
const mockStoredToken = {
tokenHash: 'somehash',
isRevoked: true,
expiresAt: new Date(Date.now() + 10000),
};
mockTokenRepo.findOne.mockResolvedValue(mockStoredToken);
await expect(service.refreshToken(1, 'revoked_token')).rejects.toThrow(
UnauthorizedException
);
});
});
});

View File

@@ -1,18 +1,142 @@
import { Test, TestingModule } from '@nestjs/testing';
import { FileStorageService } from './file-storage.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Attachment } from './entities/attachment.entity';
import { ConfigService } from '@nestjs/config';
import * as fs from 'fs-extra';
import {
BadRequestException,
NotFoundException,
ForbiddenException,
} from '@nestjs/common';
import { Repository } from 'typeorm';
// Mock fs-extra
jest.mock('fs-extra');
describe('FileStorageService', () => {
let service: FileStorageService;
let attachmentRepo: Repository<Attachment>;
const mockAttachment = {
id: 1,
originalFilename: 'test.pdf',
storedFilename: 'uuid.pdf',
filePath: '/permanent/2024/12/uuid.pdf',
fileSize: 1024,
uploadedByUserId: 1,
} as Attachment;
const mockFile = {
originalname: 'test.pdf',
mimetype: 'application/pdf',
size: 1024,
buffer: Buffer.from('test-content'),
} as Express.Multer.File;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [FileStorageService],
providers: [
FileStorageService,
{
provide: getRepositoryToken(Attachment),
useValue: {
create: jest.fn().mockReturnValue(mockAttachment),
save: jest.fn().mockResolvedValue(mockAttachment),
find: jest.fn(),
findOne: jest.fn(),
remove: jest.fn(),
},
},
{
provide: ConfigService,
useValue: {
get: jest.fn((key) => {
if (key === 'NODE_ENV') return 'test';
return null;
}),
},
},
],
}).compile();
service = module.get<FileStorageService>(FileStorageService);
attachmentRepo = module.get(getRepositoryToken(Attachment));
jest.clearAllMocks();
(fs.ensureDirSync as jest.Mock).mockReturnValue(true);
(fs.writeFile as jest.Mock).mockResolvedValue(undefined);
(fs.pathExists as jest.Mock).mockResolvedValue(true);
(fs.move as jest.Mock).mockResolvedValue(undefined);
(fs.remove as jest.Mock).mockResolvedValue(undefined);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('upload', () => {
it('should save file to temp and create DB record', async () => {
const result = await service.upload(mockFile, 1);
expect(fs.writeFile).toHaveBeenCalled();
expect(attachmentRepo.create).toHaveBeenCalled();
expect(attachmentRepo.save).toHaveBeenCalled();
expect(result).toBeDefined();
});
it('should throw BadRequestException if write fails', async () => {
(fs.writeFile as jest.Mock).mockRejectedValueOnce(
new Error('Write error')
);
await expect(service.upload(mockFile, 1)).rejects.toThrow(
BadRequestException
);
});
});
describe('commit', () => {
it('should move files to permanent storage', async () => {
const tempIds = ['uuid-1'];
const mockAttachments = [
{
...mockAttachment,
isTemporary: true,
tempId: 'uuid-1',
filePath: '/temp/uuid.pdf',
},
];
(attachmentRepo.find as jest.Mock).mockResolvedValue(mockAttachments);
await service.commit(tempIds);
expect(fs.ensureDir).toHaveBeenCalled();
expect(fs.move).toHaveBeenCalled();
expect(attachmentRepo.save).toHaveBeenCalled();
});
it('should show warning if file counts mismatch', async () => {
(attachmentRepo.find as jest.Mock).mockResolvedValue([]);
await expect(service.commit(['uuid-1'])).rejects.toThrow(
NotFoundException
);
});
});
describe('delete', () => {
it('should delete file if user owns it', async () => {
(attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);
await service.delete(1, 1);
expect(fs.remove).toHaveBeenCalled();
expect(attachmentRepo.remove).toHaveBeenCalled();
});
it('should throw ForbiddenException if user does not own file', async () => {
(attachmentRepo.findOne as jest.Mock).mockResolvedValue(mockAttachment);
await expect(service.delete(1, 999)).rejects.toThrow(ForbiddenException);
});
});
});