test(security): add and refactor api security/idor coverage
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { PantryService } from './pantry.service';
|
||||
|
||||
describe('PantryService IDOR security', () => {
|
||||
const makeCreatePantryDto = (overrides: Partial<{ productId: number; userId: number }> = {}) => ({
|
||||
productId: 1,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const prismaMock = {
|
||||
pantryItem: {
|
||||
findMany: jest.fn(),
|
||||
findUnique: jest.fn(),
|
||||
findFirst: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
product: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
user: {
|
||||
findUnique: jest.fn(),
|
||||
},
|
||||
inventoryItem: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
$transaction: jest.fn(),
|
||||
};
|
||||
|
||||
const service = new PantryService(prismaMock as any);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const mockPantryFindManyEmpty = () => {
|
||||
prismaMock.pantryItem.findMany.mockResolvedValue([]);
|
||||
};
|
||||
|
||||
it('scopar findAll till userId', async () => {
|
||||
mockPantryFindManyEmpty();
|
||||
|
||||
await service.findAll(42);
|
||||
|
||||
expect(prismaMock.pantryItem.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ userId: 42 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('findAllAdmin kan filtrera per userId', async () => {
|
||||
mockPantryFindManyEmpty();
|
||||
|
||||
await service.findAllAdmin({ userId: 99 });
|
||||
|
||||
expect(prismaMock.pantryItem.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({ userId: 99 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('findAllAdmin returnerar alla items om userId inte specificeras', async () => {
|
||||
mockPantryFindManyEmpty();
|
||||
|
||||
await service.findAllAdmin({});
|
||||
|
||||
expect(prismaMock.pantryItem.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('create sätter userId på nya pantry-items', async () => {
|
||||
prismaMock.product.findUnique.mockResolvedValue({ id: 1 });
|
||||
prismaMock.pantryItem.create.mockResolvedValue({ id: 1, userId: 42 });
|
||||
|
||||
await service.create(42, makeCreatePantryDto() as any);
|
||||
|
||||
expect(prismaMock.pantryItem.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ userId: 42, productId: 1 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('createAdmin kan skapa pantry-item för explicit userId', async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue({ id: 99 });
|
||||
prismaMock.product.findUnique.mockResolvedValue({ id: 1 });
|
||||
prismaMock.pantryItem.create.mockResolvedValue({ id: 1, userId: 99 });
|
||||
|
||||
await service.createAdmin(1, makeCreatePantryDto({ userId: 99 }) as any, 99);
|
||||
|
||||
expect(prismaMock.pantryItem.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ userId: 99, productId: 1 }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('createAdmin throws NotFoundException om target user saknas', async () => {
|
||||
prismaMock.user.findUnique.mockResolvedValue(null);
|
||||
|
||||
await expect(service.createAdmin(1, makeCreatePantryDto({ userId: 999 }) as any, 999)).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
|
||||
it('remove nekar icke-owner', async () => {
|
||||
// Simulera att item inte hittas (userId matchar inte)
|
||||
prismaMock.pantryItem.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(service.remove(42, 1)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('moveToInventory nekar icke-owner', async () => {
|
||||
// Simulera att item inte hittas (userId matchar inte)
|
||||
prismaMock.pantryItem.findFirst.mockResolvedValue(null);
|
||||
|
||||
await expect(service.moveToInventory(42, 1, {} as any)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ForbiddenException, UnauthorizedException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import { PantryController } from './pantry.controller';
|
||||
import { getRolesMetadata, mockHttpContext } from '../test-utils/security-test-helpers';
|
||||
|
||||
describe('Pantry admin security', () => {
|
||||
const adminHandlers: Array<[string, Function]> = [
|
||||
['findAllAdmin', PantryController.prototype.findAllAdmin],
|
||||
['createAdmin', PantryController.prototype.createAdmin],
|
||||
['updateAdmin', PantryController.prototype.updateAdmin],
|
||||
['removeAdmin', PantryController.prototype.removeAdmin],
|
||||
['moveToInventoryAdmin', PantryController.prototype.moveToInventoryAdmin],
|
||||
];
|
||||
|
||||
it('alla admin-endpoints har @Roles("admin") metadata', () => {
|
||||
for (const [, handler] of adminHandlers) {
|
||||
expect(getRolesMetadata(handler)).toEqual(['admin']);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(adminHandlers)('RolesGuard nekar icke-admin (403) på %s', (_name, handler) => {
|
||||
const reflector = new Reflector();
|
||||
const guard = new RolesGuard(reflector);
|
||||
const context = mockHttpContext({
|
||||
handler,
|
||||
clazz: PantryController,
|
||||
user: { role: 'user' },
|
||||
});
|
||||
|
||||
expect(() => guard.canActivate(context)).toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it.each(adminHandlers)('RolesGuard tillåter admin (200/allow) på %s', (_name, handler) => {
|
||||
const reflector = new Reflector();
|
||||
const guard = new RolesGuard(reflector);
|
||||
const context = mockHttpContext({
|
||||
handler,
|
||||
clazz: PantryController,
|
||||
user: { role: 'admin' },
|
||||
});
|
||||
|
||||
expect(guard.canActivate(context)).toBe(true);
|
||||
});
|
||||
|
||||
it('JwtAuthGuard mappar saknad användare till 401', () => {
|
||||
const guard = new JwtAuthGuard(new Reflector());
|
||||
|
||||
expect(() => guard.handleRequest(null, null, null)).toThrow(UnauthorizedException);
|
||||
});
|
||||
|
||||
it('JwtAuthGuard släpper igenom autentiserad användare (200/allow)', () => {
|
||||
const guard = new JwtAuthGuard(new Reflector());
|
||||
const user = { userId: 42, role: 'user' };
|
||||
|
||||
expect(guard.handleRequest(null, user, null)).toBe(user);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user