diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index 3d58f84c..ee79f445 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -1,34 +1,214 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { mock } from 'jest-mock-extended'; import { PantriesController } from './pantries.controller'; import { PantriesService } from './pantries.service'; -import { OrdersService } from '../orders/order.service'; -import { Order } from '../orders/order.entity'; +import { Pantry } from './pantries.entity'; +import { User } from '../users/user.entity'; +import { Role } from '../users/types'; +import { mock } from 'jest-mock-extended'; +import { PantryApplicationDto } from './dtos/pantry-application.dto'; const mockPantriesService = mock(); +import { OrdersService } from '../orders/order.service'; +import { Order } from '../orders/order.entity'; const mockOrdersService = mock(); +import { + Activity, + AllergensConfidence, + ClientVisitFrequency, + PantryStatus, + RefrigeratedDonation, + ReserveFoodForAllergic, + ServeAllergicChildren, +} from './types'; describe('PantriesController', () => { let controller: PantriesController; - beforeEach(async () => { - jest.clearAllMocks(); + const mockUser = { + id: 1, + role: Role.VOLUNTEER, + firstName: 'John', + lastName: 'Doe', + email: '', + phone: '123-456-7890', + }; + // Mock Pantry + const mockPantry = { + pantryId: 1, + pantryName: 'Test Pantry', + status: PantryStatus.PENDING, + } as Pantry; + + // Mock Pantry Application + const mockPantryApplication = { + contactFirstName: 'Jane', + contactLastName: 'Smith', + contactEmail: 'jane.smith@example.com', + contactPhone: '(508) 222-2222', + hasEmailContact: true, + emailContactOther: null, + secondaryContactFirstName: 'John', + secondaryContactLastName: 'Doe', + secondaryContactEmail: 'john.doe@example.com', + secondaryContactPhone: '(508) 333-3333', + shipmentAddressLine1: '456 New St', + shipmentAddressLine2: 'Suite 200', + shipmentAddressCity: 'Cambridge', + shipmentAddressState: 'MA', + shipmentAddressZip: '02139', + shipmentAddressCountry: 'USA', + acceptFoodDeliveries: true, + deliveryWindowInstructions: 'Please deliver between 9am-5pm', + mailingAddressLine1: '456 New St', + mailingAddressLine2: 'Suite 200', + mailingAddressCity: 'Cambridge', + mailingAddressState: 'MA', + mailingAddressZip: '02139', + mailingAddressCountry: 'USA', + pantryName: 'New Community Pantry', + allergenClients: '10 to 20', + restrictions: ['Peanut allergy', 'Gluten'], + refrigeratedDonation: RefrigeratedDonation.YES, + dedicatedAllergyFriendly: true, + reserveFoodForAllergic: ReserveFoodForAllergic.SOME, + reservationExplanation: 'We have a dedicated allergen-free section', + clientVisitFrequency: ClientVisitFrequency.DAILY, + identifyAllergensConfidence: AllergensConfidence.VERY_CONFIDENT, + serveAllergicChildren: ServeAllergicChildren.YES_MANY, + activities: [Activity.CREATE_LABELED_SHELF, Activity.COLLECT_FEEDBACK], + activitiesComments: 'We provide nutritional counseling', + itemsInStock: 'Canned goods, pasta', + needMoreOptions: 'More fresh produce', + newsletterSubscription: true, + } as PantryApplicationDto; + + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [PantriesController], providers: [ - { provide: PantriesService, useValue: mockPantriesService }, - { provide: OrdersService, useValue: mockOrdersService }, + { + provide: PantriesService, + useValue: mockPantriesService, + }, ], }).compile(); controller = module.get(PantriesController); }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should be defined', () => { expect(controller).toBeDefined(); }); + describe('getPendingPantries', () => { + it('should return an array of pending pantries', async () => { + mockPantriesService.getPendingPantries.mockResolvedValueOnce([ + mockPantry, + ] as Pantry[]); + + const result = await controller.getPendingPantries(); + + expect(result).toEqual([mockPantry] as Pantry[]); + expect(mockPantriesService.getPendingPantries).toHaveBeenCalled(); + }); + + it('should return an empty array if no pending pantries', async () => { + mockPantriesService.getPendingPantries.mockResolvedValueOnce([]); + + const result = await controller.getPendingPantries(); + + expect(result).toEqual([]); + expect(mockPantriesService.getPendingPantries).toHaveBeenCalled(); + }); + }); + + describe('getPantry', () => { + it('should return a single pantry by id', async () => { + mockPantriesService.findOne.mockResolvedValueOnce(mockPantry as Pantry); + + const result = await controller.getPantry(1); + + expect(result).toEqual(mockPantry as Pantry); + expect(mockPantriesService.findOne).toHaveBeenCalledWith(1); + }); + + it('should throw NotFoundException if pantry does not exist', async () => { + mockPantriesService.findOne.mockRejectedValueOnce( + new Error('Pantry 999 not found'), + ); + + await expect(controller.getPantry(999)).rejects.toThrow(); + expect(mockPantriesService.findOne).toHaveBeenCalledWith(999); + }); + }); + + describe('approvePantry', () => { + it('should approve a pantry', async () => { + mockPantriesService.approve.mockResolvedValueOnce(undefined); + + await controller.approvePantry(1); + + expect(mockPantriesService.approve).toHaveBeenCalledWith(1); + }); + + it('should throw error if pantry does not exist', async () => { + mockPantriesService.approve.mockRejectedValueOnce( + new Error('Pantry 999 not found'), + ); + + await expect(controller.approvePantry(999)).rejects.toThrow(); + expect(mockPantriesService.approve).toHaveBeenCalledWith(999); + }); + }); + + describe('denyPantry', () => { + it('should deny a pantry', async () => { + mockPantriesService.deny.mockResolvedValueOnce(undefined); + + await controller.denyPantry(1); + + expect(mockPantriesService.deny).toHaveBeenCalledWith(1); + }); + + it('should throw error if pantry does not exist', async () => { + mockPantriesService.deny.mockRejectedValueOnce( + new Error('Pantry 999 not found'), + ); + + await expect(controller.denyPantry(999)).rejects.toThrow(); + expect(mockPantriesService.deny).toHaveBeenCalledWith(999); + }); + }); + + describe('submitPantryApplication', () => { + it('should submit a pantry application successfully', async () => { + mockPantriesService.addPantry.mockResolvedValueOnce(undefined); + + await controller.submitPantryApplication(mockPantryApplication); + + expect(mockPantriesService.addPantry).toHaveBeenCalledWith( + mockPantryApplication, + ); + }); + + it('should throw error if application data is invalid', async () => { + mockPantriesService.addPantry.mockRejectedValueOnce( + new Error('Invalid application data'), + ); + + await expect( + controller.submitPantryApplication(mockPantryApplication), + ).rejects.toThrow(); + expect(mockPantriesService.addPantry).toHaveBeenCalledWith( + mockPantryApplication, + ); + }); + }); describe('getOrders', () => { it('should return orders for a pantry', async () => { const pantryId = 24; diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts new file mode 100644 index 00000000..5f2ebc3a --- /dev/null +++ b/apps/backend/src/pantries/pantries.service.spec.ts @@ -0,0 +1,294 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PantriesService } from './pantries.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Pantry } from './pantries.entity'; +import { Repository } from 'typeorm'; +import { NotFoundException, Res } from '@nestjs/common'; +import { Role } from '../users/types'; +import { mock } from 'jest-mock-extended'; +import { PantryApplicationDto } from './dtos/pantry-application.dto'; +import { + ClientVisitFrequency, + PantryStatus, + RefrigeratedDonation, + ServeAllergicChildren, +} from './types'; +import { ReserveFoodForAllergic } from './types'; +import { Activity } from './types'; +import { AllergensConfidence } from './types'; +import { StatusType } from '@aws-sdk/client-cognito-identity-provider'; + +const mockRepository = mock>(); + +describe('PantriesService', () => { + let service: PantriesService; + + const mockUser = { + id: 1, + role: Role.VOLUNTEER, + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phone: '123-456-7890', + }; + + // Mock Pantry + const mockPendingPantry = { + pantryId: 1, + pantryName: 'Test Pantry', + status: PantryStatus.PENDING, + } as Pantry; + + // Mock Pantry Application + const mockPantryApplication = { + contactFirstName: 'Jane', + contactLastName: 'Smith', + contactEmail: 'jane.smith@example.com', + contactPhone: '(508) 222-2222', + hasEmailContact: true, + emailContactOther: null, + secondaryContactFirstName: 'John', + secondaryContactLastName: 'Doe', + secondaryContactEmail: 'john.doe@example.com', + secondaryContactPhone: '(508) 333-3333', + shipmentAddressLine1: '456 New St', + shipmentAddressLine2: 'Suite 200', + shipmentAddressCity: 'Cambridge', + shipmentAddressState: 'MA', + shipmentAddressZip: '02139', + shipmentAddressCountry: 'USA', + acceptFoodDeliveries: true, + deliveryWindowInstructions: 'Please deliver between 9am-5pm', + mailingAddressLine1: '456 New St', + mailingAddressLine2: 'Suite 200', + mailingAddressCity: 'Cambridge', + mailingAddressState: 'MA', + mailingAddressZip: '02139', + mailingAddressCountry: 'USA', + pantryName: 'New Community Pantry', + allergenClients: '10 to 20', + restrictions: ['Peanut allergy', 'Gluten'], + refrigeratedDonation: RefrigeratedDonation.YES, + dedicatedAllergyFriendly: true, + reserveFoodForAllergic: ReserveFoodForAllergic.SOME, + reservationExplanation: 'We have a dedicated allergen-free section', + clientVisitFrequency: ClientVisitFrequency.DAILY, + identifyAllergensConfidence: AllergensConfidence.VERY_CONFIDENT, + serveAllergicChildren: ServeAllergicChildren.YES_MANY, + activities: [Activity.CREATE_LABELED_SHELF, Activity.COLLECT_FEEDBACK], + activitiesComments: 'We provide nutritional counseling', + itemsInStock: 'Canned goods, pasta', + needMoreOptions: 'More fresh produce', + newsletterSubscription: true, + } as PantryApplicationDto; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PantriesService, + { + provide: getRepositoryToken(Pantry), + useValue: mockRepository, + }, + ], + }).compile(); + + service = module.get(PantriesService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + // Find pantry by ID + describe('findOne', () => { + it('should return a pantry by id', async () => { + mockRepository.findOne.mockResolvedValueOnce(mockPendingPantry); + + const result = await service.findOne(1); + + expect(result).toBe(mockPendingPantry); + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { pantryId: 1 }, + }); + }); + + it('should throw NotFoundException if pantry not found', async () => { + mockRepository.findOne.mockResolvedValueOnce(null); + + await expect(service.findOne(999)).rejects.toThrow(NotFoundException); + await expect(service.findOne(999)).rejects.toThrow( + 'Pantry 999 not found', + ); + }); + }); + + // Get pantries with pending status + describe('getPendingPantries', () => { + it('should return only pending pantries', async () => { + const pendingPantries = [ + mockPendingPantry, + { ...mockPendingPantry, pantryId: 3 }, + ]; + mockRepository.find.mockResolvedValueOnce(pendingPantries); + + const result = await service.getPendingPantries(); + + expect(result).toEqual(pendingPantries); + expect(result).toHaveLength(2); + expect(result.every((pantry) => pantry.status === 'pending')).toBe(true); + expect(mockRepository.find).toHaveBeenCalledWith({ + where: { status: 'pending' }, + relations: ['pantryUser'], + }); + }); + + it('should not return approved pantries', async () => { + mockRepository.find.mockResolvedValueOnce([mockPendingPantry]); + + const result = await service.getPendingPantries(); + + expect(result).not.toContainEqual( + expect.objectContaining({ status: 'approved' }), + ); + expect(mockRepository.find).toHaveBeenCalledWith({ + where: { status: 'pending' }, + relations: ['pantryUser'], + }); + }); + + it('should return an empty array if no pending pantries', async () => { + mockRepository.find.mockResolvedValueOnce([]); + + const result = await service.getPendingPantries(); + + expect(result).toEqual([]); + }); + }); + + // Approve pantry by ID (status = approved) + describe('approve', () => { + it('should approve a pantry', async () => { + mockRepository.findOne.mockResolvedValueOnce(mockPendingPantry); + mockRepository.update.mockResolvedValueOnce(undefined); + + await service.approve(1); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { pantryId: 1 }, + }); + expect(mockRepository.update).toHaveBeenCalledWith(1, { + status: 'approved', + }); + }); + + it('should throw NotFoundException if pantry not found', async () => { + mockRepository.findOne.mockResolvedValueOnce(null); + + await expect(service.approve(999)).rejects.toThrow(NotFoundException); + await expect(service.approve(999)).rejects.toThrow( + 'Pantry 999 not found', + ); + expect(mockRepository.update).not.toHaveBeenCalled(); + }); + }); + + // Deny pantry by ID (status = denied) + describe('deny', () => { + it('should deny a pantry', async () => { + mockRepository.findOne.mockResolvedValueOnce(mockPendingPantry); + mockRepository.update.mockResolvedValueOnce(undefined); + + await service.deny(1); + + expect(mockRepository.findOne).toHaveBeenCalledWith({ + where: { pantryId: 1 }, + }); + expect(mockRepository.update).toHaveBeenCalledWith(1, { + status: 'denied', + }); + }); + + it('should throw NotFoundException if pantry not found', async () => { + mockRepository.findOne.mockResolvedValueOnce(null); + + await expect(service.deny(999)).rejects.toThrow(NotFoundException); + await expect(service.deny(999)).rejects.toThrow('Pantry 999 not found'); + expect(mockRepository.update).not.toHaveBeenCalled(); + }); + }); + + // Add pantry + describe('addPantry', () => { + it('should add a new pantry application', async () => { + mockRepository.save.mockResolvedValueOnce(mockPendingPantry); + + await service.addPantry(mockPantryApplication); + + expect(mockRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + pantryName: mockPantryApplication.pantryName, + shipmentAddressLine1: mockPantryApplication.shipmentAddressLine1, + shipmentAddressLine2: mockPantryApplication.shipmentAddressLine2, + shipmentAddressCity: mockPantryApplication.shipmentAddressCity, + shipmentAddressState: mockPantryApplication.shipmentAddressState, + shipmentAddressZip: mockPantryApplication.shipmentAddressZip, + shipmentAddressCountry: mockPantryApplication.shipmentAddressCountry, + mailingAddressLine1: mockPantryApplication.mailingAddressLine1, + mailingAddressLine2: mockPantryApplication.mailingAddressLine2, + mailingAddressCity: mockPantryApplication.mailingAddressCity, + mailingAddressState: mockPantryApplication.mailingAddressState, + mailingAddressZip: mockPantryApplication.mailingAddressZip, + mailingAddressCountry: mockPantryApplication.mailingAddressCountry, + allergenClients: mockPantryApplication.allergenClients, + restrictions: mockPantryApplication.restrictions, + refrigeratedDonation: mockPantryApplication.refrigeratedDonation, + reserveFoodForAllergic: mockPantryApplication.reserveFoodForAllergic, + reservationExplanation: mockPantryApplication.reservationExplanation, + dedicatedAllergyFriendly: + mockPantryApplication.dedicatedAllergyFriendly, + clientVisitFrequency: mockPantryApplication.clientVisitFrequency, + identifyAllergensConfidence: + mockPantryApplication.identifyAllergensConfidence, + serveAllergicChildren: mockPantryApplication.serveAllergicChildren, + activities: mockPantryApplication.activities, + activitiesComments: mockPantryApplication.activitiesComments, + itemsInStock: mockPantryApplication.itemsInStock, + needMoreOptions: mockPantryApplication.needMoreOptions, + newsletterSubscription: true, + }), + ); + }); + + it('should create pantry representative from contact info', async () => { + mockRepository.save.mockResolvedValueOnce(mockPendingPantry); + + await service.addPantry(mockPantryApplication); + + expect(mockRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + pantryUser: expect.objectContaining({ + firstName: mockPantryApplication.contactFirstName, + lastName: mockPantryApplication.contactLastName, + email: mockPantryApplication.contactEmail, + phone: mockPantryApplication.contactPhone, + role: 'pantry', + }), + }), + ); + }); + + it('should throw error if save fails', async () => { + mockRepository.save.mockRejectedValueOnce(new Error('Database error')); + + await expect(service.addPantry(mockPantryApplication)).rejects.toThrow( + 'Database error', + ); + expect(mockRepository.save).toHaveBeenCalled(); + }); + }); +});