import { IS_PUBLIC_KEY } from '../auth/decorators/public.decorator'; import { ProductsController } from './products.controller'; import { getRolesMetadata } from '../test-utils/security-test-helpers'; describe('Products controller security', () => { const productsServiceMock = { findByOwner: jest.fn(), findOne: jest.fn(), createPrivate: jest.fn(), createPending: jest.fn(), updateCanonicalNamePrivate: jest.fn(), mergePrivate: jest.fn(), updateCategoryMine: jest.fn(), }; const aiServiceMock = { suggestCategory: jest.fn(), }; const categoriesServiceMock = { findFlattened: jest.fn(), }; const controller = new ProductsController( productsServiceMock as any, aiServiceMock as any, categoriesServiceMock as any, ); beforeEach(() => { jest.clearAllMocks(); }); it.each([ ['findAll', ProductsController.prototype.findAll], ['findAllTags', ProductsController.prototype.findAllTags], ])('endpoint %s har @Public metadata', (_name, handler) => { const isPublic = Reflect.getMetadata(IS_PUBLIC_KEY, handler) as boolean | undefined; expect(isPublic).toBe(true); }); it('alla admin-endpoints har @Roles("admin") metadata', () => { for (const [, handler] of [ ['findDuplicates', ProductsController.prototype.findDuplicates], ['previewMerge', ProductsController.prototype.previewMerge], ['backfillCanonical', ProductsController.prototype.backfillCanonical], ['findPending', ProductsController.prototype.findPending], ['findPrivate', ProductsController.prototype.findPrivate], ['aiCategorizeBulk', ProductsController.prototype.aiCategorizeBulk], ['findDeleted', ProductsController.prototype.findDeleted], ['create', ProductsController.prototype.create], ['merge', ProductsController.prototype.merge], ['updateCanonicalName', ProductsController.prototype.updateCanonicalName], ['setTags', ProductsController.prototype.setTags], ['upsertNutrition', ProductsController.prototype.upsertNutrition], ['update', ProductsController.prototype.update], ['promotePrivateToGlobal', ProductsController.prototype.promotePrivateToGlobal], ['permanentDelete', ProductsController.prototype.permanentDelete], ['remove', ProductsController.prototype.remove], ['setStatus', ProductsController.prototype.setStatus], ['restore', ProductsController.prototype.restore], ['resetAll', ProductsController.prototype.resetAll], ['bulkUpdate', ProductsController.prototype.bulkUpdate], ]) { expect(getRolesMetadata(handler as Function)).toEqual(['admin']); } }); it('findMine vidarebefordrar req.user.id till owner-scope', () => { productsServiceMock.findByOwner.mockResolvedValue([]); controller.findMine({ user: { id: 42 } } as any); expect(productsServiceMock.findByOwner).toHaveBeenCalledWith(42); }); it('createPrivate vidarebefordrar req.user.id', () => { const dto = { name: 'Private Product' }; productsServiceMock.createPrivate.mockResolvedValue({ id: 1 }); controller.createPrivate(dto as any, { user: { id: 42 } } as any); expect(productsServiceMock.createPrivate).toHaveBeenCalledWith(dto, 42); }); it('createPending vidarebefordrar req.user.id', () => { const dto = { name: 'Pending Product' }; productsServiceMock.createPending.mockResolvedValue({ id: 1 }); controller.createPending(dto as any, { user: { id: 42 } } as any); expect(productsServiceMock.createPending).toHaveBeenCalledWith(dto, 42); }); it('updateCanonicalNamePrivate vidarebefordrar req.user.id', () => { productsServiceMock.updateCanonicalNamePrivate.mockResolvedValue({ id: 1 }); controller.updateCanonicalNamePrivate(1, { canonicalName: 'milk' } as any, { user: { id: 42 } } as any); expect(productsServiceMock.updateCanonicalNamePrivate).toHaveBeenCalledWith(42, 1, 'milk'); }); it('mergePrivate vidarebefordrar req.user.id', () => { productsServiceMock.mergePrivate.mockResolvedValue({ merged: true }); controller.mergePrivate({ sourceProductId: 10, targetProductId: 20 } as any, { user: { id: 42 } } as any); expect(productsServiceMock.mergePrivate).toHaveBeenCalledWith(42, 10, 20); }); it('updateCategoryMine vidarebefordrar req.user.id', () => { productsServiceMock.updateCategoryMine.mockResolvedValue({ id: 1, categoryId: 5 }); controller.updateCategoryMine(1, { categoryId: 5 } as any, { user: { id: 42 } } as any); expect(productsServiceMock.updateCategoryMine).toHaveBeenCalledWith(42, 1, 5); }); it('suggestCategory använder canonicalName fallback name', async () => { productsServiceMock.findOne.mockResolvedValue({ id: 1, name: 'Mjolk', canonicalName: 'Mjolk 1L' }); categoriesServiceMock.findFlattened.mockResolvedValue([{ id: 1, name: 'Mejeri' }]); aiServiceMock.suggestCategory.mockResolvedValue({ categoryId: 1 }); await controller.suggestCategory(1); expect(aiServiceMock.suggestCategory).toHaveBeenCalledWith('Mjolk 1L', [{ id: 1, name: 'Mejeri' }]); }); });