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); }); });