Files
recipe-app/backend/src/recipes/recipes.idor.spec.ts
T
Nils-Johan Gynther 1db30c9b6f
Test Suite / test (24.15.0) (push) Has been cancelled
test(security): add and refactor api security/idor coverage
2026-05-11 16:40:16 +02:00

150 lines
3.9 KiB
TypeScript

import { NotFoundException } from '@nestjs/common';
import { RecipesService } from './recipes.service';
describe('RecipesService IDOR security', () => {
const prismaMock = {
recipe: {
findUnique: jest.fn(),
findFirst: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
},
recipeIngredient: {
findMany: jest.fn(),
create: jest.fn(),
deleteMany: jest.fn(),
},
product: {
findMany: jest.fn(),
findFirst: jest.fn(),
},
user: {
findUnique: jest.fn(),
},
recipeShare: {
findUnique: jest.fn(),
create: jest.fn(),
delete: jest.fn(),
},
$transaction: jest.fn(),
};
const aiServiceMock = {};
const recipeMatchingServiceMock = {};
const service = new RecipesService(prismaMock as any, aiServiceMock as any, recipeMatchingServiceMock as any);
beforeEach(() => {
jest.clearAllMocks();
});
const setRecipeAsOwnedByAnotherUser = () => {
prismaMock.recipe.findUnique.mockResolvedValue({
id: 1,
ownerId: 100,
});
};
it('findAll scopar resultaten till userId (owner eller shared)', async () => {
prismaMock.recipe.findMany.mockResolvedValue([]);
await service.findAll(42);
expect(prismaMock.recipe.findMany).toHaveBeenCalledWith(
expect.objectContaining({
where: expect.objectContaining({
OR: expect.arrayContaining([
{ ownerId: 42 },
{ isPublic: true },
{ shares: { some: { userId: 42 } } },
]),
}),
}),
);
});
it('findOne nekar access om recipe inte är owner/shared/public', async () => {
prismaMock.recipe.findFirst.mockResolvedValue(null);
await expect(service.findOne(1, 42)).rejects.toThrow(NotFoundException);
});
it('findOne tillåter owner att läsa eget recipe', async () => {
prismaMock.recipe.findFirst.mockResolvedValue({
id: 1,
ownerId: 42,
isPublic: false,
name: 'Test Recipe',
description: '',
instructions: '',
mealPlanDayOfWeek: null,
imageUrl: null,
createdAt: new Date(),
updatedAt: new Date(),
});
const result = await service.findOne(1, 42);
expect(result).toBeDefined();
});
it('findOne tillåter shared user att läsa shared recipe', async () => {
prismaMock.recipe.findFirst.mockResolvedValue({
id: 1,
ownerId: 100,
isPublic: false,
shares: [{ userId: 42 }],
});
const result = await service.findOne(1, 42);
expect(result).toBeDefined();
});
it('findOne tillåter alla att läsa public recipe', async () => {
prismaMock.recipe.findFirst.mockResolvedValue({
id: 1,
ownerId: 100,
isPublic: true,
});
const result = await service.findOne(1, 42);
expect(result).toBeDefined();
});
it.each([
{
name: 'update',
action: () => service.update(1, {} as any, 42),
},
{
name: 'remove',
action: () => service.remove(1, 42),
},
{
name: 'shareWithUser',
action: () => service.shareWithUser(1, 42, 'someuser'),
},
{
name: 'unshareWithUser',
action: () => service.unshareWithUser(1, 42, 'someuser'),
},
{
name: 'setVisibility',
action: () => service.setVisibility(1, 42, true),
},
{
name: 'addIngredient',
action: () => service.addIngredient(1, { productId: 1, quantity: 100 } as any, 42),
},
{
name: 'updateImage',
action: () => service.updateImage(1, 'http://example.com/image.jpg', 42),
},
])('%s nekar icke-owner', async ({ action }) => {
setRecipeAsOwnedByAnotherUser();
await expect(action()).rejects.toThrow(NotFoundException);
});
});