diff --git a/.env.example b/.env.example index c0ee816e6..74b39ad5b 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ DATABASE_HOST=localhost DATABASE_PORT=5432 DATABASE_NAME=securing-safe-food +DATABASE_NAME_TEST=securing-safe-food-test DATABASE_USERNAME=postgres DATABASE_PASSWORD=PLACEHOLDER_PASSWORD \ No newline at end of file diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml new file mode 100644 index 000000000..0bd508da1 --- /dev/null +++ b/.github/workflows/backend-tests.yml @@ -0,0 +1,30 @@ +on: [push, pull_request] + +jobs: + backend-tests: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:15 + env: + POSTGRES_DB: securing-safe-food-test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + env: + DATABASE_HOST: 127.0.0.1 + DATABASE_PORT: 5432 + DATABASE_NAME_TEST: securing-safe-food-test + DATABASE_USERNAME: postgres + DATABASE_PASSWORD: postgres + NX_DAEMON: 'false' + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - run: yarn install + - run: yarn test + + diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml deleted file mode 100644 index bf0fcbef9..000000000 --- a/.github/workflows/ci-cd.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: CI/CD - -# First runs linter and tests all affected projects -# Then, for each project that requires deployment, deploy-- is added -# Environment variables are labelled _SHORT_DESCRIPTION - -on: - push: - branches: ['main'] - pull_request: - branches: ['main'] - workflow_dispatch: - inputs: - manual-deploy: - description: 'App to Deploy' - required: false - default: '' - -concurrency: - # Never have two deployments happening at the same time (potential race condition) - group: '{{ github.head_ref || github.ref }}' - -jobs: - pre-deploy: - runs-on: ubuntu-latest - outputs: - affected: ${{ steps.should-deploy.outputs.affected }} - steps: - - uses: actions/checkout@v3 - with: - # We need to fetch all branches and commits so that Nx affected has a base to compare against. - fetch-depth: 0 - - name: Use Node.js 20 - uses: actions/setup-node@v3 - with: - node-version: 20.x - cache: 'yarn' - - - name: Install Dependencies - run: yarn install - - # In any subsequent steps within this job (myjob) we can reference the resolved SHAs - # using either the step outputs or environment variables: - - name: Derive appropriate SHAs for base and head for `nx affected` commands - uses: nrwl/nx-set-shas@v3 - - - run: | - echo "BASE: ${{ env.NX_BASE }}" - echo "HEAD: ${{ env.NX_HEAD }}" - - - name: Nx Affected Lint - run: npx nx affected -t lint - - # - name: Nx Affected Test - # run: npx nx affected -t test - - - name: Nx Affected Build - run: npx nx affected -t build - - - name: Determine who needs to be deployed - id: should-deploy - run: | - echo "The following projects have been affected: [$(npx nx print-affected -t build --select=tasks.target.project)]"; - echo "affected=$(npx nx print-affected -t build --select=tasks.target.project)" >> "$GITHUB_OUTPUT" - - deploy-debug: - needs: pre-deploy - runs-on: ubuntu-latest - steps: - - name: Debug logs - run: | - echo "Manual Deploy: ${{github.event.inputs.manual-deploy}}"; - echo "Affected Names: ${{needs.pre-deploy.outputs.affected}}"; - echo "Event: ${{github.event_name}}"; - echo "Ref: ${{github.ref}}"; - echo "Will deploy?: ${{(github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main'}}"; - - deploy-frontend: - needs: pre-deploy - if: (contains(github.event.inputs.manual-deploy, 'c4c-ops-frontend') || contains(needs.pre-deploy.outputs.affected, 'scaffolding-frontend')) && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - # For "simplicity", deployment settings are configured in the AWS Amplify Console - # This just posts to a webhook telling Amplify to redeploy the main branch - steps: - - name: Tell Amplify to rebuild - run: curl -X POST -d {} ${C4C_OPS_WEBHOOK_DEPLOY} -H "Content-Type:application/json" - env: - C4C_OPS_WEBHOOK_DEPLOY: ${{ secrets.C4C_OPS_WEBHOOK_DEPLOY }} - - deploy-backend: - needs: pre-deploy - if: (contains(github.event.inputs.manual-deploy, 'c4c-ops-backend') || contains(needs.pre-deploy.outputs.affected, 'scaffolding-backend')) && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Use Node.js 16 - uses: actions/setup-node@v3 - with: - node-version: 16.x - cache: 'yarn' - - - name: Install Dependencies - run: yarn install - - - run: npx nx build c4c-ops-backend --configuration production - - name: default deploy - uses: appleboy/lambda-action@master - with: - aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws_region: ${{ secrets.AWS_REGION }} - function_name: c4c-ops-monolith-lambda - source: dist/apps/c4c-ops/c4c-ops-backend/main.js diff --git a/apps/backend/README.md b/apps/backend/README.md index 8e4e513c0..9eec729f5 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -25,4 +25,12 @@ You can check that your database connection details are correct by running `nx s "LOG 🚀 Application is running on: http://localhost:3000/api" ``` -Finally, run `yarn run typeorm:migrate` to load all the tables into your database. If everything is set up correctly, you should see "Migration ... has been executed successfully." in the terminal. \ No newline at end of file +Finally, run `yarn run typeorm:migrate` to load all the tables into your database. If everything is set up correctly, you should see "Migration ... has been executed successfully." in the terminal. + +### Running backend tests + +1. Create a **separate** Postgres database (for example `securing-safe-food-test`). +2. Add a `DATABASE_NAME_TEST` entry (and optionally `DATABASE_HOST/PORT/USERNAME/PASSWORD`) to your `.env` so the test data source can connect to that database. +3. Run the backend test suite with `npx jest`. + +Each spec builds up the database and tables, tears it all down, and runs all the migrations on each tests. This ensures that we always have the most up to date data that we test with. \ No newline at end of file diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts new file mode 100644 index 000000000..9573736c8 --- /dev/null +++ b/apps/backend/src/config/migrations.ts @@ -0,0 +1,59 @@ +import { User1725726359198 } from '../migrations/1725726359198-User'; +import { AddTables1726524792261 } from '../migrations/1726524792261-addTables'; +import { ReviseTables1737522923066 } from '../migrations/1737522923066-reviseTables'; +import { UpdateUserRole1737816745912 } from '../migrations/1737816745912-UpdateUserRole'; +import { UpdatePantriesTable1737906317154 } from '../migrations/1737906317154-updatePantriesTable'; +import { UpdateDonations1738697216020 } from '../migrations/1738697216020-updateDonations'; +import { UpdateDonationColTypes1741708808976 } from '../migrations/1741708808976-UpdateDonationColTypes'; +import { UpdatePantriesTable1738172265266 } from '../migrations/1738172265266-updatePantriesTable'; +import { UpdatePantriesTable1739056029076 } from '../migrations/1739056029076-updatePantriesTable'; +import { AssignmentsPantryIdNotUnique1758384669652 } from '../migrations/1758384669652-AssignmentsPantryIdNotUnique'; +import { UpdateOrdersTable1740367964915 } from '../migrations/1740367964915-updateOrdersTable'; +import { UpdateFoodRequests1744051370129 } from '../migrations/1744051370129-updateFoodRequests'; +import { UpdateRequestTable1741571847063 } from '../migrations/1741571847063-updateRequestTable'; +import { RemoveOrderIdFromRequests1744133526650 } from '../migrations/1744133526650-removeOrderIdFromRequests'; +import { AddOrders1739496585940 } from '../migrations/1739496585940-addOrders'; +import { AddingEnumValues1760538239997 } from '../migrations/1760538239997-AddingEnumValues'; +import { UpdateColsToUseEnumType1760886499863 } from '../migrations/1760886499863-UpdateColsToUseEnumType'; +import { UpdatePantriesTable1742739750279 } from '../migrations/1742739750279-updatePantriesTable'; +import { RemoveOrdersDonationId1761500262238 } from '../migrations/1761500262238-RemoveOrdersDonationId'; +import { AddVolunteerPantryUniqueConstraint1760033134668 } from '../migrations/1760033134668-AddVolunteerPantryUniqueConstraint'; +import { AllergyFriendlyToBoolType1763963056712 } from '../migrations/1763963056712-AllergyFriendlyToBoolType'; +import { UpdatePantryUserFieldsFixed1764350314832 } from '../migrations/1764350314832-UpdatePantryUserFieldsFixed'; +import { RemoveMultipleVolunteerTypes1764811878152 } from '../migrations/1764811878152-RemoveMultipleVolunteerTypes'; +import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-RemoveUnusedStatuses'; +import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields'; +import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData'; +import { RemovePantryFromOrders1769316004958 } from '../migrations/1769316004958-RemovePantryFromOrders'; + +const schemaMigrations = [ + User1725726359198, + AddTables1726524792261, + ReviseTables1737522923066, + UpdateUserRole1737816745912, + UpdatePantriesTable1737906317154, + UpdateDonations1738697216020, + UpdateDonationColTypes1741708808976, + UpdatePantriesTable1738172265266, + UpdatePantriesTable1739056029076, + AssignmentsPantryIdNotUnique1758384669652, + AddOrders1739496585940, + UpdateOrdersTable1740367964915, + UpdateRequestTable1741571847063, + UpdateFoodRequests1744051370129, + RemoveOrderIdFromRequests1744133526650, + AddingEnumValues1760538239997, + UpdateColsToUseEnumType1760886499863, + UpdatePantriesTable1742739750279, + RemoveOrdersDonationId1761500262238, + UpdatePantryFields1763762628431, + AddVolunteerPantryUniqueConstraint1760033134668, + AllergyFriendlyToBoolType1763963056712, + UpdatePantryUserFieldsFixed1764350314832, + RemoveMultipleVolunteerTypes1764811878152, + RemoveUnusedStatuses1764816885341, + PopulateDummyData1768501812134, + RemovePantryFromOrders1769316004958, +]; + +export default schemaMigrations; diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index 10c8a5af2..7f7ce75d3 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -1,33 +1,7 @@ import { registerAs } from '@nestjs/config'; import { PluralNamingStrategy } from '../strategies/plural-naming.strategy'; import { DataSource, DataSourceOptions } from 'typeorm'; -import { User1725726359198 } from '../migrations/1725726359198-User'; -import { AddTables1726524792261 } from '../migrations/1726524792261-addTables'; -import { ReviseTables1737522923066 } from '../migrations/1737522923066-reviseTables'; -import { UpdateUserRole1737816745912 } from '../migrations/1737816745912-UpdateUserRole'; -import { UpdatePantriesTable1737906317154 } from '../migrations/1737906317154-updatePantriesTable'; -import { UpdateDonations1738697216020 } from '../migrations/1738697216020-updateDonations'; -import { UpdateDonationColTypes1741708808976 } from '../migrations/1741708808976-UpdateDonationColTypes'; -import { UpdatePantriesTable1738172265266 } from '../migrations/1738172265266-updatePantriesTable'; -import { UpdatePantriesTable1739056029076 } from '../migrations/1739056029076-updatePantriesTable'; -import { AssignmentsPantryIdNotUnique1758384669652 } from '../migrations/1758384669652-AssignmentsPantryIdNotUnique'; -import { UpdateOrdersTable1740367964915 } from '../migrations/1740367964915-updateOrdersTable'; -import { UpdateFoodRequests1744051370129 } from '../migrations/1744051370129-updateFoodRequests'; -import { UpdateRequestTable1741571847063 } from '../migrations/1741571847063-updateRequestTable'; -import { RemoveOrderIdFromRequests1744133526650 } from '../migrations/1744133526650-removeOrderIdFromRequests'; -import { AddOrders1739496585940 } from '../migrations/1739496585940-addOrders'; -import { AddingEnumValues1760538239997 } from '../migrations/1760538239997-AddingEnumValues'; -import { UpdateColsToUseEnumType1760886499863 } from '../migrations/1760886499863-UpdateColsToUseEnumType'; -import { UpdatePantriesTable1742739750279 } from '../migrations/1742739750279-updatePantriesTable'; -import { RemoveOrdersDonationId1761500262238 } from '../migrations/1761500262238-RemoveOrdersDonationId'; -import { AddVolunteerPantryUniqueConstraint1760033134668 } from '../migrations/1760033134668-AddVolunteerPantryUniqueConstraint'; -import { AllergyFriendlyToBoolType1763963056712 } from '../migrations/1763963056712-AllergyFriendlyToBoolType'; -import { UpdatePantryUserFieldsFixed1764350314832 } from '../migrations/1764350314832-UpdatePantryUserFieldsFixed'; -import { RemoveMultipleVolunteerTypes1764811878152 } from '../migrations/1764811878152-RemoveMultipleVolunteerTypes'; -import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-RemoveUnusedStatuses'; -import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields'; -import { PopulateDummyData1768501812134 } from '../migrations/1768501812134-populateDummyData'; -import { RemovePantryFromOrders1769316004958 } from '../migrations/1769316004958-RemovePantryFromOrders'; +import schemaMigrations from './migrations'; const config = { type: 'postgres', @@ -39,37 +13,7 @@ const config = { autoLoadEntities: true, synchronize: false, namingStrategy: new PluralNamingStrategy(), - // Glob patterns (e.g. ../migrations/**.ts) are deprecated, so we have to manually specify each migration - // TODO: see if there's still a way to dynamically load all migrations - migrations: [ - User1725726359198, - AddTables1726524792261, - ReviseTables1737522923066, - UpdateUserRole1737816745912, - UpdatePantriesTable1737906317154, - UpdateDonations1738697216020, - UpdateDonationColTypes1741708808976, - UpdatePantriesTable1738172265266, - UpdatePantriesTable1739056029076, - AssignmentsPantryIdNotUnique1758384669652, - AddOrders1739496585940, - UpdateOrdersTable1740367964915, - UpdateRequestTable1741571847063, - UpdateFoodRequests1744051370129, - RemoveOrderIdFromRequests1744133526650, - AddingEnumValues1760538239997, - UpdateColsToUseEnumType1760886499863, - UpdatePantriesTable1742739750279, - RemoveOrdersDonationId1761500262238, - UpdatePantryFields1763762628431, - AddVolunteerPantryUniqueConstraint1760033134668, - AllergyFriendlyToBoolType1763963056712, - UpdatePantryUserFieldsFixed1764350314832, - RemoveMultipleVolunteerTypes1764811878152, - RemoveUnusedStatuses1764816885341, - PopulateDummyData1768501812134, - RemovePantryFromOrders1769316004958, - ], + migrations: [...schemaMigrations], }; export default registerAs('typeorm', () => config); diff --git a/apps/backend/src/config/typeormTestDataSource.ts b/apps/backend/src/config/typeormTestDataSource.ts new file mode 100644 index 000000000..9c8810ff0 --- /dev/null +++ b/apps/backend/src/config/typeormTestDataSource.ts @@ -0,0 +1,36 @@ +import 'dotenv/config'; +import { DataSource, DataSourceOptions } from 'typeorm'; +import { PluralNamingStrategy } from '../strategies/plural-naming.strategy'; +import { Order } from '../orders/order.entity'; +import { Pantry } from '../pantries/pantries.entity'; +import { User } from '../users/user.entity'; +import { Donation } from '../donations/donations.entity'; +import { FoodManufacturer } from '../foodManufacturers/manufacturer.entity'; +import { FoodRequest } from '../foodRequests/request.entity'; +import { DonationItem } from '../donationItems/donationItems.entity'; +import { Allocation } from '../allocations/allocations.entity'; +import schemaMigrations from './migrations'; + +const testConfig: DataSourceOptions = { + type: 'postgres', + host: process.env.DATABASE_HOST ?? '127.0.0.1', + port: process.env.DATABASE_PORT ? parseInt(process.env.DATABASE_PORT) : 5432, + database: process.env.DATABASE_NAME_TEST ?? 'securing-safe-food-test', + username: process.env.DATABASE_USERNAME ?? 'postgres', + password: process.env.DATABASE_PASSWORD ?? 'postgres', + synchronize: false, + namingStrategy: new PluralNamingStrategy(), + entities: [ + Order, + Pantry, + User, + Donation, + FoodManufacturer, + FoodRequest, + DonationItem, + Allocation, + ], + migrations: schemaMigrations, +}; + +export const testDataSource = new DataSource(testConfig); diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 3fd0b1f2f..e8e41949e 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -1,64 +1,37 @@ -import { Test } from '@nestjs/testing'; +import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository, SelectQueryBuilder } from 'typeorm'; -import { Order } from './order.entity'; import { OrdersService } from './order.service'; -import { mock } from 'jest-mock-extended'; -import { Pantry } from '../pantries/pantries.entity'; -import { User } from '../users/user.entity'; -import { - AllergensConfidence, - ClientVisitFrequency, - PantryStatus, - RefrigeratedDonation, - ServeAllergicChildren, -} from '../pantries/types'; +import { Order } from './order.entity'; +import { testDataSource } from '../config/typeormTestDataSource'; import { OrderStatus } from './types'; -import { FoodRequest } from '../foodRequests/request.entity'; - -const mockOrdersRepository = mock>(); -const mockPantryRepository = mock>(); +import { Pantry } from '../pantries/pantries.entity'; -const mockPantry: Partial = { - pantryId: 1, - pantryName: 'Test Pantry', - allergenClients: '', - refrigeratedDonation: RefrigeratedDonation.NO, - reserveFoodForAllergic: 'Yes', - reservationExplanation: '', - dedicatedAllergyFriendly: false, - clientVisitFrequency: ClientVisitFrequency.DAILY, - identifyAllergensConfidence: AllergensConfidence.NOT_VERY_CONFIDENT, - serveAllergicChildren: ServeAllergicChildren.NO, - newsletterSubscription: false, - restrictions: [], - pantryUser: null as unknown as User, - status: PantryStatus.APPROVED, - dateApplied: new Date(), - activities: [], - activitiesComments: '', - itemsInStock: '', - needMoreOptions: '', - volunteers: [], -}; +// Set 1 minute timeout for async DB operations +jest.setTimeout(60000); describe('OrdersService', () => { let service: OrdersService; - let qb: SelectQueryBuilder; beforeAll(async () => { - mockOrdersRepository.createQueryBuilder.mockReset(); + // Initialize DataSource once + if (!testDataSource.isInitialized) { + await testDataSource.initialize(); + } - const module = await Test.createTestingModule({ + // Clean database at the start + await testDataSource.query(`DROP SCHEMA public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); + + const module: TestingModule = await Test.createTestingModule({ providers: [ OrdersService, { provide: getRepositoryToken(Order), - useValue: mockOrdersRepository, + useValue: testDataSource.getRepository(Order), }, { provide: getRepositoryToken(Pantry), - useValue: mockPantryRepository, + useValue: testDataSource.getRepository(Pantry), }, ], }).compile(); @@ -66,16 +39,22 @@ describe('OrdersService', () => { service = module.get(OrdersService); }); - beforeEach(() => { - qb = { - leftJoinAndSelect: jest.fn().mockReturnThis(), - leftJoin: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([]), - } as unknown as SelectQueryBuilder; + beforeEach(async () => { + // Run all migrations fresh for each test + await testDataSource.runMigrations(); + }); - mockOrdersRepository.createQueryBuilder.mockReturnValue(qb); + afterEach(async () => { + // Drop the schema completely (cascades all tables) + await testDataSource.query(`DROP SCHEMA public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); + }); + + afterAll(async () => { + // Destroy all schemas + if (testDataSource.isInitialized) { + await testDataSource.destroy(); + } }); it('should be defined', () => { @@ -83,212 +62,63 @@ describe('OrdersService', () => { }); describe('getAll', () => { - it('should return orders filtered by status', async () => { - const mockOrders: Partial[] = [ - { orderId: 1, status: OrderStatus.PENDING }, - { orderId: 2, status: OrderStatus.DELIVERED }, - ]; - - (qb.getMany as jest.Mock).mockResolvedValue([mockOrders[0] as Order]); + it('returns orders filtered by status', async () => { + const orders = await service.getAll({ status: OrderStatus.DELIVERED }); - const result = await service.getAll({ status: OrderStatus.PENDING }); - - expect(result).toEqual([mockOrders[0]]); - expect(qb.andWhere).toHaveBeenCalledWith('order.status = :status', { - status: OrderStatus.PENDING, - }); + expect(orders).toHaveLength(2); + expect( + orders.every((order) => order.status === OrderStatus.DELIVERED), + ).toBe(true); }); - it('should return empty array when no status filters match', async () => { - (qb.getMany as jest.Mock).mockResolvedValue([]); - - const result = await service.getAll({ status: 'invalid status' }); - - expect(result).toEqual([]); - expect(qb.andWhere).toHaveBeenCalledWith('order.status = :status', { - status: 'invalid status', - }); - }); - - it('should return orders filtered by pantryName', async () => { - const mockOrders: Partial[] = [ - { - orderId: 3, - status: OrderStatus.DELIVERED, - }, - { - orderId: 4, - status: OrderStatus.DELIVERED, - }, - { - orderId: 5, - status: OrderStatus.DELIVERED, - }, - ]; - - (qb.getMany as jest.Mock).mockResolvedValue( - mockOrders.slice(0, 2) as Order[], + it('returns empty array when status filter matches nothing', async () => { + // Delete allocations referencing pending orders, then delete orders themselves + await testDataSource.query( + `DELETE FROM "allocations" WHERE order_id IN (SELECT order_id FROM "orders" WHERE status = $1)`, + [OrderStatus.PENDING], ); + await testDataSource.query(`DELETE FROM "orders" WHERE status = $1`, [ + OrderStatus.PENDING, + ]); - const result = await service.getAll({ - pantryNames: ['Test Pantry', 'Test Pantry 2'], - }); - - expect(result).toEqual(mockOrders.slice(0, 2) as Order[]); - expect(qb.andWhere).toHaveBeenCalledWith( - 'pantry.pantryName IN (:...pantryNames)', - { pantryNames: ['Test Pantry', 'Test Pantry 2'] }, - ); + const orders = await service.getAll({ status: OrderStatus.PENDING }); + expect(orders).toEqual([]); }); - it('should return empty array when no pantryName filters match', async () => { - (qb.getMany as jest.Mock).mockResolvedValue([]); - - const result = await service.getAll({ - pantryNames: ['Nonexistent Pantry'], + it('returns orders filtered by pantry names', async () => { + const orders = await service.getAll({ + pantryNames: ['Community Food Pantry Downtown'], }); - expect(result).toEqual([]); - expect(qb.andWhere).toHaveBeenCalledWith( - 'pantry.pantryName IN (:...pantryNames)', - { pantryNames: ['Nonexistent Pantry'] }, - ); + expect(orders).toHaveLength(2); + expect( + orders.every( + (order) => + order.request.pantry.pantryName === + 'Community Food Pantry Downtown', + ), + ).toBe(true); }); - it('should return orders filtered by both status and pantryName', async () => { - const mockOrders: Partial[] = [ - { - orderId: 3, - status: OrderStatus.DELIVERED, - }, - { - orderId: 4, - status: OrderStatus.DELIVERED, - }, - { - orderId: 5, - status: OrderStatus.DELIVERED, - }, - ]; - - (qb.getMany as jest.Mock).mockResolvedValue( - mockOrders.slice(1, 3) as Order[], - ); - - const result = await service.getAll({ - status: 'delivered', - pantryNames: ['Test Pantry 2'], + it('returns empty array when pantry filter matches nothing', async () => { + const orders = await service.getAll({ + pantryNames: ['Nonexistent Pantry'], }); - expect(result).toEqual(mockOrders.slice(1, 3) as Order[]); - expect(qb.andWhere).toHaveBeenCalledWith('order.status = :status', { - status: 'delivered', - }); - expect(qb.andWhere).toHaveBeenCalledWith( - 'pantry.pantryName IN (:...pantryNames)', - { pantryNames: ['Test Pantry 2'] }, - ); + expect(orders).toEqual([]); }); - }); - - describe('findOrderPantry', () => { - it('should return pantry for given order', async () => { - const mockFoodRequest: Partial = { - requestId: 1, - pantryId: 1, - }; - - const mockOrder: Partial = { - orderId: 1, - requestId: 1, - request: mockFoodRequest as FoodRequest, - }; - - (mockOrdersRepository.findOne as jest.Mock).mockResolvedValue(mockOrder); - (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue( - mockPantry as Pantry, - ); - - const result = await service.findOrderPantry(1); - expect(result).toEqual(mockPantry); - expect(mockPantryRepository.findOneBy).toHaveBeenCalledWith({ - pantryId: 1, + it('returns orders filtered by both pantry and status', async () => { + const orders = await service.getAll({ + status: OrderStatus.DELIVERED, + pantryNames: ['Westside Community Kitchen'], }); - }); - - it('should throw NotFoundException if order not found', async () => { - (mockOrdersRepository.findOne as jest.Mock).mockResolvedValue(null); - await expect(service.findOrderPantry(999)).rejects.toThrow( - 'Order 999 not found', + expect(orders).toHaveLength(1); + expect(orders[0].request.pantry.pantryName).toBe( + 'Westside Community Kitchen', ); - }); - - it('should throw NotFoundException if pantry not found', async () => { - const mockFoodRequest: Partial = { - requestId: 1, - pantryId: 999, - }; - - const mockOrder: Partial = { - orderId: 1, - requestId: 1, - request: mockFoodRequest as FoodRequest, - }; - - (mockOrdersRepository.findOne as jest.Mock).mockResolvedValue(mockOrder); - (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue(null); - - await expect(service.findOrderPantry(1)).rejects.toThrow( - 'Pantry 999 not found', - ); - }); - }); - - describe('getOrdersByPantry', () => { - it('should return orders for given pantry', async () => { - const mockOrders: Partial[] = [ - { orderId: 1, requestId: 1 }, - { orderId: 2, requestId: 2 }, - ]; - - (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue( - mockPantry as Pantry, - ); - (mockOrdersRepository.find as jest.Mock).mockResolvedValue( - mockOrders as Order[], - ); - - const result = await service.getOrdersByPantry(1); - - expect(result).toEqual(mockOrders); - expect(mockPantryRepository.findOneBy).toHaveBeenCalledWith({ - pantryId: 1, - }); - expect(mockOrdersRepository.find).toHaveBeenCalledWith({ - where: { request: { pantryId: 1 } }, - relations: ['request'], - }); - }); - - it('should throw NotFoundException if pantry does not exist', async () => { - (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue(null); - - await expect(service.getOrdersByPantry(999)).rejects.toThrow( - 'Pantry 999 not found', - ); - }); - - it('should return empty array if pantry has no orders', async () => { - (mockPantryRepository.findOneBy as jest.Mock).mockResolvedValue( - mockPantry as Pantry, - ); - (mockOrdersRepository.find as jest.Mock).mockResolvedValue([]); - - const result = await service.getOrdersByPantry(1); - - expect(result).toEqual([]); + expect(orders[0].status).toBe(OrderStatus.DELIVERED); }); }); }); diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index 3c46cb858..5a63830da 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -284,7 +284,12 @@ const RequestDetailsModal: React.FC = ({ mx={3} /> - + {item.quantity} diff --git a/yarn.lock b/yarn.lock index 0f6ccee40..e3755f070 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15987,4 +15987,4 @@ yocto-queue@^0.1.0: yocto-queue@^1.0.0: version "1.2.2" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.2.tgz#3e09c95d3f1aa89a58c114c99223edf639152c00" - integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== + integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== \ No newline at end of file