fix: fix peer1 doesnt recognize cli. tests: add unit test for user module

This commit is contained in:
yosaphatprs 2025-12-11 13:22:09 +07:00
parent f61d86036d
commit d11dc9b2a9
4 changed files with 697 additions and 9 deletions

View File

@ -1,18 +1,308 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller'; import { UserController } from './user.controller';
import { UserService } from './user.service';
import { AuthGuard } from '../auth/guard/auth.guard';
import { RolesGuard } from '../auth/guard/roles.guard';
import { UserRole } from '../auth/dto/auth.dto';
import type { Request, Response } from 'express';
describe('UserController', () => { describe('UserController', () => {
let controller: UserController; let controller: UserController;
let mockUserService: {
getAllUsers: jest.Mock;
};
const mockUsersResponse = {
'0': {
id: BigInt(1),
name: 'John Doe',
username: 'johndoe',
role: UserRole.Admin,
created_at: new Date('2024-01-01'),
updated_at: new Date('2024-01-02'),
},
'1': {
id: BigInt(2),
name: 'Jane Smith',
username: 'janesmith',
role: UserRole.User,
created_at: new Date('2024-01-03'),
updated_at: new Date('2024-01-04'),
},
totalCount: 2,
};
beforeEach(async () => { beforeEach(async () => {
mockUserService = {
getAllUsers: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
controllers: [UserController], controllers: [UserController],
}).compile(); providers: [{ provide: UserService, useValue: mockUserService }],
})
.overrideGuard(AuthGuard)
.useValue({ canActivate: () => true })
.overrideGuard(RolesGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<UserController>(UserController); controller = module.get<UserController>(UserController);
}); });
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => { it('should be defined', () => {
expect(controller).toBeDefined(); expect(controller).toBeDefined();
}); });
// ============================================================
// getUserProfile
// ============================================================
describe('getUserProfile', () => {
it('should return static profile string with id', () => {
const result = controller.getUserProfile('123');
expect(result).toBe('User profile data 123');
});
it('should return profile string for any id value', () => {
expect(controller.getUserProfile('abc')).toBe('User profile data abc');
expect(controller.getUserProfile('')).toBe('User profile data ');
expect(controller.getUserProfile('999')).toBe('User profile data 999');
});
// ISSUE: This endpoint returns a static string - likely unimplemented
// It doesn't actually fetch user profile data
it('ISSUE: returns static string instead of actual user data', () => {
const result = controller.getUserProfile('1');
// This is just a placeholder, not real profile data
expect(typeof result).toBe('string');
expect(result).not.toContain('{'); // Not JSON
});
});
// ============================================================
// getAllUsers
// ============================================================
describe('getAllUsers', () => {
it('should call userService.getAllUsers with query params', async () => {
mockUserService.getAllUsers.mockResolvedValue(mockUsersResponse);
const result = await controller.getAllUsers('john', 1, 10);
expect(mockUserService.getAllUsers).toHaveBeenCalledWith({
username: 'john',
page: 1,
take: 10,
});
expect(result).toEqual(mockUsersResponse);
});
it('should handle undefined query params', async () => {
mockUserService.getAllUsers.mockResolvedValue(mockUsersResponse);
// Query params can be undefined when not provided
await controller.getAllUsers(
undefined as unknown as string,
undefined as unknown as number,
undefined as unknown as number,
);
expect(mockUserService.getAllUsers).toHaveBeenCalledWith({
username: undefined,
page: undefined,
take: undefined,
});
});
it('should pass through service errors', async () => {
mockUserService.getAllUsers.mockRejectedValue(new Error('Service error'));
await expect(controller.getAllUsers('john', 1, 10)).rejects.toThrow(
'Service error',
);
});
// ISSUE: Query params come as strings, but typed as numbers
// NestJS will NOT auto-convert them without @Transform or ParseIntPipe
it('ISSUE: page and take come as strings from query but typed as number', async () => {
mockUserService.getAllUsers.mockResolvedValue(mockUsersResponse);
// In real scenario, these would be strings '1' and '10'
// But TypeScript types them as number - potential type mismatch
await controller.getAllUsers('john', '1' as any, '10' as any);
expect(mockUserService.getAllUsers).toHaveBeenCalledWith({
username: 'john',
page: '1', // Still a string, not number
take: '10',
});
});
});
// ============================================================
// setCookie
// ============================================================
describe('setCookie', () => {
let mockResponse: Partial<Response>;
beforeEach(() => {
mockResponse = {
cookie: jest.fn(),
status: jest.fn().mockReturnThis(),
send: jest.fn(),
};
});
it('should set cookie and return success message', () => {
controller.setCookie('testName', mockResponse as Response);
expect(mockResponse.cookie).toHaveBeenCalledWith('name', 'testName');
expect(mockResponse.status).toHaveBeenCalledWith(200);
expect(mockResponse.send).toHaveBeenCalledWith(
"Cookie 'name' set to 'testName'",
);
});
it('should handle empty string name', () => {
controller.setCookie('', mockResponse as Response);
expect(mockResponse.cookie).toHaveBeenCalledWith('name', '');
expect(mockResponse.send).toHaveBeenCalledWith("Cookie 'name' set to ''");
});
it('should handle special characters in name', () => {
controller.setCookie(
'<script>alert("xss")</script>',
mockResponse as Response,
);
// No sanitization - potential XSS in response
expect(mockResponse.cookie).toHaveBeenCalledWith(
'name',
'<script>alert("xss")</script>',
);
});
// SECURITY ISSUE: No authentication guard on this endpoint
it('SECURITY: endpoint has no authentication guard', () => {
// This test documents that setCookie is publicly accessible
// Anyone can set cookies - might be intentional for demo, but risky
expect(true).toBe(true); // Placeholder - real security test would check guards
});
});
// ============================================================
// getCookie
// ============================================================
describe('getCookie', () => {
it('should return cookie value from request', () => {
const mockRequest = {
cookies: { name: 'testValue' },
} as Partial<Request>;
const result = controller.getCookie(mockRequest as Request);
expect(result).toBe("Cookie 'testValue'");
});
it('should handle missing cookie', () => {
const mockRequest = {
cookies: {},
} as Partial<Request>;
const result = controller.getCookie(mockRequest as Request);
expect(result).toBe("Cookie 'undefined'");
});
it('should handle undefined cookies object', () => {
const mockRequest = {
cookies: undefined,
} as any;
// This will throw - no null check
expect(() => controller.getCookie(mockRequest)).toThrow();
});
// SECURITY ISSUE: No authentication guard on this endpoint
it('SECURITY: endpoint has no authentication guard', () => {
// This test documents that getCookie is publicly accessible
expect(true).toBe(true);
});
});
// ============================================================
// Guards Integration Tests (Decorator verification)
// ============================================================
describe('Guards and Decorators', () => {
it('getAllUsers should have AuthGuard and RolesGuard', () => {
// Verify decorators are applied by checking metadata
// In real tests, guards are mocked, but we document the expected behavior
const guards = Reflect.getMetadata(
'__guards__',
UserController.prototype.getAllUsers,
);
// Guards metadata exists (even though mocked in tests)
// In production, these guards would enforce authentication and role checks
});
it('getAllUsers should require Admin role', () => {
// The @Roles(UserRole.Admin) decorator restricts access
// In production, non-admin users would get 403 Forbidden
});
it('ISSUE: getUserProfile has no guards - publicly accessible', () => {
// This endpoint returns user data but has no authentication
// Potential information disclosure if real data is returned
});
it('ISSUE: cookie endpoints have no guards - publicly accessible', () => {
// setCookie and getCookie have no authentication
// May be intentional for demo but worth reviewing
});
});
}); });
/*
* ============================================================
* CODE ISSUES DOCUMENTATION
* ============================================================
*
* 1. ISSUE - getUserProfile is a stub:
* - Returns static string `User profile data ${id}`
* - Doesn't fetch actual user data
* - Fix: Implement actual profile retrieval via UserService
*
* 2. ISSUE - Query param type mismatch:
* - `page` and `take` are typed as `number` but come as strings
* - NestJS doesn't auto-convert without @Transform or ParseIntPipe
* - Fix: Use @Query('page', ParseIntPipe) or handle string parsing
*
* 3. SECURITY - No guards on getUserProfile:
* - User profile endpoint should require authentication
* - Fix: Add @UseGuards(AuthGuard) to getUserProfile
*
* 4. SECURITY - No guards on cookie endpoints:
* - setCookie and getCookie are publicly accessible
* - If demo code, consider removing; if needed, add guards
*
* 5. ISSUE - No null check in getCookie:
* - If req.cookies is undefined, accessing ['name'] throws
* - Fix: Add null check: req.cookies?.['name']
*
* 6. SECURITY - No input sanitization in setCookie response:
* - Cookie value is echoed directly in response
* - Potential reflected XSS if response is rendered as HTML
* - Fix: Sanitize output or ensure Content-Type prevents XSS
*/

View File

@ -1,18 +1,410 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service'; import { UserService } from './user.service';
import { PrismaService } from '../prisma/prisma.service';
import { UserRole } from '../auth/dto/auth.dto';
describe('UserService', () => { describe('UserService', () => {
let service: UserService; let service: UserService;
let mockPrismaService: {
users: {
findMany: jest.Mock;
count: jest.Mock;
};
};
const mockUsers = [
{
id: BigInt(1),
nama_lengkap: 'John Doe',
username: 'johndoe',
password: 'hashedpassword123',
role: 'admin',
created_at: new Date('2024-01-01'),
updated_at: new Date('2024-01-02'),
},
{
id: BigInt(2),
nama_lengkap: 'Jane Smith',
username: 'janesmith',
password: 'hashedpassword456',
role: 'user',
created_at: new Date('2024-01-03'),
updated_at: new Date('2024-01-04'),
},
{
id: BigInt(3),
nama_lengkap: 'Bob Wilson',
username: 'bobwilson',
password: 'hashedpassword789',
role: 'user',
created_at: null,
updated_at: null,
},
];
beforeEach(async () => { beforeEach(async () => {
mockPrismaService = {
users: {
findMany: jest.fn(),
count: jest.fn(),
},
};
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [UserService], providers: [
UserService,
{ provide: PrismaService, useValue: mockPrismaService },
],
}).compile(); }).compile();
service = module.get<UserService>(UserService); service = module.get<UserService>(UserService);
}); });
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
describe('getAllUsers', () => {
it('should return users with default pagination', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockResolvedValue(3);
const result = await service.getAllUsers({});
const expectedWhere = {
username: {
contains: undefined,
mode: 'insensitive',
},
};
expect(mockPrismaService.users.findMany).toHaveBeenCalledWith({
skip: 0,
take: 10,
where: expectedWhere,
});
expect(mockPrismaService.users.count).toHaveBeenCalledWith({
where: expectedWhere,
});
expect(result).toHaveProperty('totalCount', 3);
const resultAny = result as any;
expect(resultAny['0']).toBeDefined();
expect(resultAny['1']).toBeDefined();
expect(resultAny['2']).toBeDefined();
});
it('should filter by username with case-insensitive contains', async () => {
const filteredUsers = [mockUsers[0]];
mockPrismaService.users.findMany.mockResolvedValue(filteredUsers);
mockPrismaService.users.count.mockResolvedValue(1);
const result = await service.getAllUsers({ username: 'john' });
const expectedWhere = {
username: {
contains: 'john',
mode: 'insensitive',
},
};
expect(mockPrismaService.users.findMany).toHaveBeenCalledWith({
skip: 0,
take: 10,
where: expectedWhere,
});
expect(mockPrismaService.users.count).toHaveBeenCalledWith({
where: expectedWhere,
});
});
it('should transform user data to response DTO format', async () => {
mockPrismaService.users.findMany.mockResolvedValue([mockUsers[0]]);
mockPrismaService.users.count.mockResolvedValue(1);
const result = await service.getAllUsers({});
const resultAny = result as any;
expect(resultAny['0']).toEqual({
id: BigInt(1),
name: 'John Doe',
username: 'johndoe',
role: UserRole.Admin,
created_at: new Date('2024-01-01'),
updated_at: new Date('2024-01-02'),
});
});
it('should handle null created_at and updated_at', async () => {
mockPrismaService.users.findMany.mockResolvedValue([mockUsers[2]]);
mockPrismaService.users.count.mockResolvedValue(1);
const result = await service.getAllUsers({});
const resultAny = result as any;
expect(resultAny['0'].created_at).toBeUndefined();
expect(resultAny['0'].updated_at).toBeUndefined();
});
// ============================================================
// Pagination Tests (FIXED)
// ============================================================
describe('Pagination', () => {
it('should apply skip and take from page parameter', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockResolvedValue(100);
await service.getAllUsers({ page: 2, take: 10 });
const callArgs = mockPrismaService.users.findMany.mock.calls[0][0];
expect(callArgs.skip).toBe(10); // (page 2 - 1) * 10 = 10
expect(callArgs.take).toBe(10);
});
it('should apply explicit skip parameter', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockResolvedValue(100);
await service.getAllUsers({ skip: 20, take: 5 });
const callArgs = mockPrismaService.users.findMany.mock.calls[0][0];
expect(callArgs.skip).toBe(20);
expect(callArgs.take).toBe(5);
});
it('should parse take as string to number', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockResolvedValue(3);
// Query params come as strings
await service.getAllUsers({ take: '15' as any });
const callArgs = mockPrismaService.users.findMany.mock.calls[0][0];
expect(callArgs.take).toBe(15);
});
it('should default take to 10 when not provided', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockResolvedValue(3);
await service.getAllUsers({});
const callArgs = mockPrismaService.users.findMany.mock.calls[0][0];
expect(callArgs.take).toBe(10);
});
});
// ============================================================
// Count Tests (FIXED)
// ============================================================
describe('Count', () => {
it('should return FILTERED count matching the where clause', async () => {
const filteredUsers = [mockUsers[0]];
mockPrismaService.users.findMany.mockResolvedValue(filteredUsers);
mockPrismaService.users.count.mockResolvedValue(1); // Filtered count
const result = await service.getAllUsers({ username: 'john' });
// count() is called WITH the same where clause as findMany
expect(mockPrismaService.users.count).toHaveBeenCalledWith({
where: {
username: {
contains: 'john',
mode: 'insensitive',
},
},
});
// Returns filtered count
expect(result.totalCount).toBe(1);
});
});
// ============================================================
// Return Format - Intentional: spreads array for frontend consumption
// ============================================================
describe('Return Format - spreads array as object (intentional)', () => {
it('should return object with numeric keys instead of proper array', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockResolvedValue(3);
const result = await service.getAllUsers({});
expect(Array.isArray(result)).toBe(false);
expect(typeof result).toBe('object');
expect(Object.keys(result)).toContain('0');
expect(Object.keys(result)).toContain('1');
expect(Object.keys(result)).toContain('2');
expect(Object.keys(result)).toContain('totalCount');
});
});
// ============================================================
// ISSUE TEST: Unused parameters
// ============================================================
describe('Unused Parameters - orderBy and order are accepted but ignored', () => {
it('should accept orderBy but not use it', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockResolvedValue(3);
await service.getAllUsers({
orderBy: { username: 'asc' },
order: 'desc',
});
const callArgs = mockPrismaService.users.findMany.mock.calls[0][0];
expect(callArgs.orderBy).toBeUndefined();
});
});
// ============================================================
// Edge Cases
// ============================================================
describe('Edge Cases', () => {
it('should handle empty result', async () => {
mockPrismaService.users.findMany.mockResolvedValue([]);
mockPrismaService.users.count.mockResolvedValue(0);
const result = await service.getAllUsers({ username: 'nonexistent' });
expect(result.totalCount).toBe(0);
// Empty spread results in object with only totalCount
expect(Object.keys(result)).toEqual(['totalCount']);
});
it('should handle database error', async () => {
mockPrismaService.users.findMany.mockRejectedValue(
new Error('Database connection failed'),
);
await expect(service.getAllUsers({})).rejects.toThrow(
'Database connection failed',
);
});
it('should handle count error', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockRejectedValue(
new Error('Count failed'),
);
await expect(service.getAllUsers({})).rejects.toThrow('Count failed');
});
it('should handle user with all UserRole values', async () => {
const adminUser = { ...mockUsers[0], role: 'admin' };
const regularUser = { ...mockUsers[1], role: 'user' };
mockPrismaService.users.findMany.mockResolvedValue([
adminUser,
regularUser,
]);
mockPrismaService.users.count.mockResolvedValue(2);
const result = await service.getAllUsers({});
const resultAny = result as any;
expect(resultAny['0'].role).toBe(UserRole.Admin);
expect(resultAny['1'].role).toBe(UserRole.User);
});
it('should handle very large page numbers', async () => {
mockPrismaService.users.findMany.mockResolvedValue([]);
mockPrismaService.users.count.mockResolvedValue(10);
// Page 1000 with 10 per page = skip 9990
// But skip is never applied, so this doesn't actually paginate
const result = await service.getAllUsers({ page: 1000, take: 10 });
expect(mockPrismaService.users.findMany).toHaveBeenCalled();
});
it('should handle undefined username filter', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockResolvedValue(3);
await service.getAllUsers({ username: undefined });
expect(mockPrismaService.users.findMany).toHaveBeenCalledWith({
skip: 0,
take: 10,
where: {
username: {
contains: undefined,
mode: 'insensitive',
},
},
});
});
it('should handle empty string username filter', async () => {
mockPrismaService.users.findMany.mockResolvedValue(mockUsers);
mockPrismaService.users.count.mockResolvedValue(3);
await service.getAllUsers({ username: '' });
expect(mockPrismaService.users.findMany).toHaveBeenCalledWith({
skip: 0,
take: 10,
where: {
username: {
contains: '',
mode: 'insensitive',
},
},
});
});
});
// ============================================================
// Security Consideration
// ============================================================
describe('Security', () => {
it('should NOT expose password field in response', async () => {
mockPrismaService.users.findMany.mockResolvedValue([mockUsers[0]]);
mockPrismaService.users.count.mockResolvedValue(1);
const result = await service.getAllUsers({});
// Password should not be in the mapped response
const resultAny = result as any;
expect(resultAny['0'].password).toBeUndefined();
expect(resultAny['0']).not.toHaveProperty('password');
});
});
});
}); });
/*
* ============================================================
* CODE ISSUES DOCUMENTATION
* ============================================================
*
* 1. BUG - Pagination not applied:
* - `skip` and `take` are computed but never passed to findMany()
* - Fix: Add skip and take to the findMany call:
* findMany({ skip: skipValue, take: take, where: {...} })
*
* 2. BUG - Count doesn't use filter:
* - count() returns total records, not filtered count
* - Fix: Pass the same where clause to count():
* count({ where: { username: { contains: username, mode: 'insensitive' } } })
*
* 3. BUG - Return format spreads array:
* - { ...usersResponse, totalCount } creates { '0': user1, '1': user2, totalCount }
* - Fix: Return { data: usersResponse, totalCount: count }
*
* 4. ISSUE - Unused parameters:
* - orderBy and order parameters are accepted but never used
* - Fix: Either implement ordering or remove the parameters
*
* 5. SUGGESTION - Explicit field selection:
* - While password is not exposed in mapping, better to explicitly select fields:
* select: { id: true, nama_lengkap: true, username: true, role: true, ... }
*/

View File

@ -22,15 +22,20 @@ export class UserService {
: page : page
? (parseInt(page.toString()) - 1) * take ? (parseInt(page.toString()) - 1) * take
: 0; : 0;
const users = await this.prisma.users.findMany({ const whereClause = {
where: { username: {
username: { contains: username,
contains: username, mode: 'insensitive' as const,
mode: 'insensitive',
},
}, },
};
const users = await this.prisma.users.findMany({
skip: skipValue,
take: take,
where: whereClause,
});
const count = await this.prisma.users.count({
where: whereClause,
}); });
const count = await this.prisma.users.count();
const usersResponse = users.map((user) => ({ const usersResponse = users.map((user) => ({
id: user.id, id: user.id,
name: user.nama_lengkap, name: user.nama_lengkap,

View File

@ -133,6 +133,7 @@ services:
- /home/labai2/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer1.hospital.com/tls:/etc/hyperledger/fabric/tls - /home/labai2/josafat/hospital-log/backend/blockchain/network/organizations/peerOrganizations/hospital.com/peers/peer1.hospital.com/tls:/etc/hyperledger/fabric/tls
extra_hosts: extra_hosts:
- "peer0.hospital.com:192.168.11.211" - "peer0.hospital.com:192.168.11.211"
- "orderer.hospital.com:192.168.11.211"
- "peer2.hospital.com:192.168.11.63" - "peer2.hospital.com:192.168.11.63"
ports: ports:
- target: 8051 - target: 8051