diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml new file mode 100644 index 00000000..314b531c --- /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: npx jest + + diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml deleted file mode 100644 index bf0fcbef..00000000 --- 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 8e4e513c..50a9a7a5 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 integration 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 truncates all data tables and runs the `CreateDummyData1759636753110` migration so every test receives the same dataset while keeping the schema created by the regular migrations. \ No newline at end of file diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index f25a2961..4a244ff0 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -18,6 +18,7 @@ import { RemoveOrderIdFromRequests1744133526650 } from '../migrations/1744133526 import { AddOrders1739496585940 } from '../migrations/1739496585940-addOrders'; import { UpdatePantriesTable1742739750279 } from '../migrations/1742739750279-updatePantriesTable'; import { RemoveOrdersDonationId1761500262238 } from '../migrations/1761500262238-RemoveOrdersDonationId'; +import { CreateDummyData1759636753110 } from '../migrations/1759636753110-createDummyData'; const config = { type: 'postgres', @@ -49,6 +50,7 @@ const config = { RemoveOrderIdFromRequests1744133526650, UpdatePantriesTable1742739750279, RemoveOrdersDonationId1761500262238, + CreateDummyData1759636753110, ], }; diff --git a/apps/backend/src/config/typeormTestDataSource.ts b/apps/backend/src/config/typeormTestDataSource.ts new file mode 100644 index 00000000..bb91b0fa --- /dev/null +++ b/apps/backend/src/config/typeormTestDataSource.ts @@ -0,0 +1,73 @@ +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 { Assignments } from '../volunteerAssignments/volunteerAssignments.entity'; + +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 { UpdatePantriesTable1738172265266 } from '../migrations/1738172265266-updatePantriesTable'; +import { UpdateDonations1738697216020 } from '../migrations/1738697216020-updateDonations'; +import { UpdatePantriesTable1739056029076 } from '../migrations/1739056029076-updatePantriesTable'; +import { AddOrders1739496585940 } from '../migrations/1739496585940-addOrders'; +import { UpdateOrdersTable1740367964915 } from '../migrations/1740367964915-updateOrdersTable'; +import { UpdateRequestTable1741571847063 } from '../migrations/1741571847063-updateRequestTable'; +import { UpdateDonationColTypes1741708808976 } from '../migrations/1741708808976-UpdateDonationColTypes'; +import { UpdatePantriesTable1742739750279 } from '../migrations/1742739750279-updatePantriesTable'; +import { UpdateFoodRequests1744051370129 } from '../migrations/1744051370129-updateFoodRequests'; +import { RemoveOrderIdFromRequests1744133526650 } from '../migrations/1744133526650-removeOrderIdFromRequests'; +import { AssignmentsPantryIdNotUnique1758384669652 } from '../migrations/1758384669652-AssignmentsPantryIdNotUnique'; +import { RemoveOrdersDonationId1761500262238 } from '../migrations/1761500262238-RemoveOrdersDonationId'; + +const testConfig: DataSourceOptions = { + type: 'postgres', + host: process.env.DATABASE_HOST ?? '127.0.0.1', + port: parseInt(process.env.DATABASE_PORT ?? '5432', 10), + 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, + Assignments, + ], + migrations: [ + User1725726359198, + AddTables1726524792261, + ReviseTables1737522923066, + UpdateUserRole1737816745912, + UpdatePantriesTable1737906317154, + UpdatePantriesTable1738172265266, + UpdateDonations1738697216020, + UpdatePantriesTable1739056029076, + AddOrders1739496585940, + UpdateOrdersTable1740367964915, + UpdateRequestTable1741571847063, + UpdateDonationColTypes1741708808976, + UpdatePantriesTable1742739750279, + UpdateFoodRequests1744051370129, + RemoveOrderIdFromRequests1744133526650, + AssignmentsPantryIdNotUnique1758384669652, + RemoveOrdersDonationId1761500262238, + ], +}; + +export const testDataSource = new DataSource(testConfig); diff --git a/apps/backend/src/migrations/1759636753110-createDummyData.ts b/apps/backend/src/migrations/1759636753110-createDummyData.ts new file mode 100644 index 00000000..fa0b4c93 --- /dev/null +++ b/apps/backend/src/migrations/1759636753110-createDummyData.ts @@ -0,0 +1,427 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateDummyData1759636753110 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + INSERT INTO public.users (first_name, last_name, email, phone, role) VALUES + ('John', 'Smith', 'john.smith@ssf.org', '555-0101', 'SSF_REPRESENTATIVE'), + ('Sarah', 'Johnson', 'sarah.j@ssf.org', '555-0102', 'SSF_REPRESENTATIVE'), + ('Mike', 'Brown', 'mike.brown@pantry1.org', '555-0201', 'PANTRY_REPRESENTATIVE'), + ('Emily', 'Davis', 'emily.davis@pantry2.org', '555-0202', 'PANTRY_REPRESENTATIVE'), + ('Robert', 'Wilson', 'robert.w@pantry3.org', '555-0203', 'PANTRY_REPRESENTATIVE'), + ('Lisa', 'Martinez', 'lisa.m@foodcorp.com', '555-0301', 'MANUFACTURER_REPRESENTATIVE'), + ('David', 'Anderson', 'david.a@healthyfoods.com', '555-0302', 'MANUFACTURER_REPRESENTATIVE'), + ('Jennifer', 'Taylor', 'jennifer.t@organic.com', '555-0303', 'MANUFACTURER_REPRESENTATIVE'), + ('James', 'Thomas', 'james.t@volunteer.org', '555-0401', 'VOLUNTEER'), + ('Maria', 'Garcia', 'maria.g@volunteer.org', '555-0402', 'VOLUNTEER'), + ('William', 'Moore', 'william.m@volunteer.org', '555-0403', 'VOLUNTEER'), + ('Patricia', 'Jackson', 'patricia.j@volunteer.org', '555-0404', 'VOLUNTEER') + `); + + await queryRunner.query(` + INSERT INTO public.food_manufacturers (food_manufacturer_name, food_manufacturer_representative_id) VALUES + ('FoodCorp Industries', (SELECT user_id FROM public.users WHERE email = 'lisa.m@foodcorp.com')), + ('Healthy Foods Co', (SELECT user_id FROM public.users WHERE email = 'david.a@healthyfoods.com')), + ('Organic Suppliers LLC', (SELECT user_id FROM public.users WHERE email = 'jennifer.t@organic.com')) + `); + + await queryRunner.query(` + INSERT INTO public.pantries ( + pantry_name, allergen_clients, refrigerated_donation, + reserve_food_for_allergic, reservation_explanation, dedicated_allergy_friendly, + client_visit_frequency, identify_allergens_confidence, serve_allergic_children, + newsletter_subscription, restrictions, ssf_representative_id, pantry_representative_id, + activities, activities_comments, items_in_stock, need_more_options, status, + address_line_1, address_city, address_state, address_zip + ) VALUES + ( + 'Community Food Pantry Downtown', + 'yes', + 'yes', + 'Yes', + 'We have several clients with severe nut allergies and need to keep separate storage', + 'Dedicated shelf for allergen-free items', + 'weekly', + 'very_confident', + 'yes', + true, + ARRAY['peanuts', 'tree_nuts', 'shellfish'], + (SELECT user_id FROM public.users WHERE email = 'john.smith@ssf.org'), + (SELECT user_id FROM public.users WHERE email = 'mike.brown@pantry1.org'), + ARRAY['Food distribution', 'nutrition education', 'cooking classes'], + 'How can we better serve clients with multiple allergies?', + 'Canned goods, pasta, rice, cereal', + 'More fresh produce and dairy alternatives', + 'active', + '123 Main Street', + 'Boston', + 'MA', + '02115' + ), + ( + 'Westside Community Kitchen', + 'some', + 'no', + 'No', + 'Limited space for separate storage', + 'None currently', + 'monthly', + 'somewhat_confident', + 'no', + true, + ARRAY['gluten'], + (SELECT user_id FROM public.users WHERE email = 'sarah.j@ssf.org'), + (SELECT user_id FROM public.users WHERE email = 'emily.davis@pantry2.org'), + ARRAY['Weekly meal service', 'food boxes'], + NULL, + 'Bread, canned vegetables, soup', + 'Gluten-free options', + 'active', + '456 Oak Avenue', + 'Boston', + 'MA', + '02116' + ), + ( + 'North End Food Bank', + 'no', + 'yes', + 'Yes', + 'Expanding allergen-friendly program', + 'Separate refrigerator for allergen-free items', + 'bi-weekly', + 'confident', + 'yes', + false, + ARRAY['dairy', 'eggs'], + (SELECT user_id FROM public.users WHERE email = 'john.smith@ssf.org'), + (SELECT user_id FROM public.users WHERE email = 'robert.w@pantry3.org'), + ARRAY['Emergency food assistance', 'senior programs'], + 'Can we get more information about cross-contamination prevention?', + 'Proteins, grains, canned fruits', + 'Dairy-free and egg-free alternatives', + 'pending', + '789 Elm Street', + 'Boston', + 'MA', + '02113' + ) + `); + + await queryRunner.query(` + INSERT INTO public.donations ( + food_manufacturer_id, date_donated, status, total_items, total_oz, total_estimated_value + ) VALUES + ( + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'FoodCorp Industries'), + '2024-01-15 10:30:00', + 'available', + 150, + 2400.50, + 850.00 + ), + ( + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'Healthy Foods Co'), + '2024-01-20 14:00:00', + 'partially_allocated', + 200, + 3200.00, + 1200.00 + ), + ( + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'Organic Suppliers LLC'), + '2024-01-25 09:15:00', + 'available', + 100, + 1600.75, + 950.00 + ), + ( + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'FoodCorp Industries'), + '2024-02-01 11:00:00', + 'fully_allocated', + 75, + 1200.00, + 450.00 + ) + `); + + await queryRunner.query(` + INSERT INTO public.donation_items ( + donation_id, item_name, quantity, reserved_quantity, status, + oz_per_item, estimated_value, food_type + ) VALUES + ( + (SELECT donation_id FROM public.donations WHERE total_items = 150), + 'Peanut Butter (16oz)', + 50, + 10, + 'available', + 16.00, + 4.50, + 'protein' + ), + ( + (SELECT donation_id FROM public.donations WHERE total_items = 150), + 'Whole Wheat Bread', + 50, + 0, + 'available', + 24.00, + 3.00, + 'grain' + ), + ( + (SELECT donation_id FROM public.donations WHERE total_items = 150), + 'Canned Green Beans', + 50, + 5, + 'available', + 8.01, + 2.00, + 'vegetable' + ), + ( + (SELECT donation_id FROM public.donations WHERE total_items = 200), + 'Gluten-Free Pasta', + 75, + 30, + 'partially_reserved', + 16.00, + 5.00, + 'grain' + ), + ( + (SELECT donation_id FROM public.donations WHERE total_items = 200), + 'Almond Milk', + 75, + 20, + 'partially_reserved', + 32.00, + 4.50, + 'dairy_alternative' + ), + ( + (SELECT donation_id FROM public.donations WHERE total_items = 200), + 'Organic Apples', + 50, + 0, + 'available', + 5.00, + 3.50, + 'fruit' + ), + ( + (SELECT donation_id FROM public.donations WHERE total_items = 100), + 'Rice (5lb bag)', + 40, + 0, + 'available', + 80.00, + 12.00, + 'grain' + ), + ( + (SELECT donation_id FROM public.donations WHERE total_items = 100), + 'Canned Tomatoes', + 60, + 0, + 'available', + 10.75, + 2.50, + 'vegetable' + ), + ( + (SELECT donation_id FROM public.donations WHERE total_items = 75), + 'Cereal Boxes', + 75, + 75, + 'fully_reserved', + 16.00, + 6.00, + 'grain' + ) + `); + + await queryRunner.query(` + INSERT INTO public.food_requests ( + pantry_id, requested_size, requested_items, additional_information, + requested_at, date_received, feedback, photos + ) VALUES + ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Community Food Pantry Downtown'), + 'large', + ARRAY['peanut_butter', 'bread', 'vegetables', 'dairy_alternatives'], + 'We have 150 families to serve this week. Need extra allergen-free options.', + '2024-01-16 08:00:00', + '2024-01-18 14:30:00', + 'Great selection, especially appreciated the allergen-free items', + ARRAY['delivery1.jpg', 'storage1.jpg'] + ), + ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Westside Community Kitchen'), + 'medium', + ARRAY['gluten_free_pasta', 'vegetables', 'fruits'], + 'Preparing meals for 75 clients this month', + '2024-01-21 09:30:00', + '2024-01-23 10:00:00', + 'Good variety, could use more gluten-free options', + ARRAY['kitchen1.jpg'] + ), + ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'North End Food Bank'), + 'small', + ARRAY['rice', 'canned_goods', 'cereal'], + 'Regular monthly order', + '2024-02-02 10:00:00', + NULL, + NULL, + NULL + ), + ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Community Food Pantry Downtown'), + 'medium', + ARRAY['cereal', 'milk_alternatives', 'fruits'], + 'Running low on breakfast items', + '2024-02-03 11:00:00', + NULL, + NULL, + NULL + ) + `); + + await queryRunner.query(` + INSERT INTO public.orders ( + request_id, pantry_id, shipped_by, status, created_at, + shipped_at, delivered_at + ) VALUES + ( + (SELECT request_id FROM public.food_requests WHERE additional_information LIKE '%150 families%'), + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Community Food Pantry Downtown'), + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'FoodCorp Industries'), + 'delivered', + '2024-01-16 09:00:00', + '2024-01-17 08:00:00', + '2024-01-18 14:30:00' + ), + ( + (SELECT request_id FROM public.food_requests WHERE additional_information LIKE '%75 clients%'), + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Westside Community Kitchen'), + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'Healthy Foods Co'), + 'delivered', + '2024-01-21 10:00:00', + '2024-01-22 09:00:00', + '2024-01-23 10:00:00' + ), + ( + (SELECT request_id FROM public.food_requests WHERE additional_information = 'Regular monthly order'), + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'North End Food Bank'), + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'Organic Suppliers LLC'), + 'shipped', + '2024-02-02 11:00:00', + '2024-02-03 08:00:00', + NULL + ), + ( + (SELECT request_id FROM public.food_requests WHERE additional_information LIKE '%breakfast items%'), + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Community Food Pantry Downtown'), + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'FoodCorp Industries'), + 'pending', + '2024-02-03 12:00:00', + NULL, + NULL + ), + ( + (SELECT request_id FROM public.food_requests WHERE additional_information LIKE '%breakfast items%'), + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Community Food Pantry Downtown'), + (SELECT food_manufacturer_id FROM public.food_manufacturers WHERE food_manufacturer_name = 'FoodCorp Industries'), + 'delivered', + '2024-02-03 12:00:00', + '2024-02-04 12:00:00', + '2024-02-05 12:00:00' + ) + `); + + await queryRunner.query(` + INSERT INTO public.allocations ( + order_id, item_id, allocated_quantity, reserved_at, fulfilled_at, status + ) VALUES + ( + (SELECT order_id FROM public.orders WHERE status = 'delivered' AND shipped_at = '2024-01-17 08:00:00'), + (SELECT item_id FROM public.donation_items WHERE item_name = 'Peanut Butter (16oz)'), + 10, + '2024-01-16 09:00:00', + '2024-01-18 14:30:00', + 'fulfilled' + ), + ( + (SELECT order_id FROM public.orders WHERE status = 'delivered' AND shipped_at = '2024-01-17 08:00:00'), + (SELECT item_id FROM public.donation_items WHERE item_name = 'Canned Green Beans'), + 5, + '2024-01-16 09:00:00', + '2024-01-18 14:30:00', + 'fulfilled' + ), + ( + (SELECT order_id FROM public.orders WHERE status = 'delivered' AND shipped_at = '2024-01-22 09:00:00'), + (SELECT item_id FROM public.donation_items WHERE item_name = 'Gluten-Free Pasta'), + 30, + '2024-01-21 10:00:00', + '2024-01-23 10:00:00', + 'fulfilled' + ), + ( + (SELECT order_id FROM public.orders WHERE status = 'delivered' AND shipped_at = '2024-01-22 09:00:00'), + (SELECT item_id FROM public.donation_items WHERE item_name = 'Almond Milk'), + 20, + '2024-01-21 10:00:00', + '2024-01-23 10:00:00', + 'fulfilled' + ), + ( + (SELECT order_id FROM public.orders WHERE status = 'pending'), + (SELECT item_id FROM public.donation_items WHERE item_name = 'Cereal Boxes'), + 75, + '2024-02-03 12:00:00', + NULL, + 'pending' + ) + `); + + await queryRunner.query(` + INSERT INTO public.volunteer_assignments (pantry_id, volunteer_id) VALUES + ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Community Food Pantry Downtown'), + (SELECT user_id FROM public.users WHERE email = 'james.t@volunteer.org') + ), + ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Community Food Pantry Downtown'), + (SELECT user_id FROM public.users WHERE email = 'patricia.j@volunteer.org') + ), + ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'Westside Community Kitchen'), + (SELECT user_id FROM public.users WHERE email = 'maria.g@volunteer.org') + ), + ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'North End Food Bank'), + (SELECT user_id FROM public.users WHERE email = 'william.m@volunteer.org') + ), + ( + (SELECT pantry_id FROM public.pantries WHERE pantry_name = 'North End Food Bank'), + (SELECT user_id FROM public.users WHERE email = 'maria.g@volunteer.org') + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM public.volunteer_assignments`); + await queryRunner.query(`DELETE FROM public.allocations`); + await queryRunner.query(`DELETE FROM public.orders`); + await queryRunner.query(`DELETE FROM public.food_requests`); + await queryRunner.query(`DELETE FROM public.donation_items`); + await queryRunner.query(`DELETE FROM public.donations`); + await queryRunner.query(`DELETE FROM public.pantries`); + await queryRunner.query(`DELETE FROM public.food_manufacturers`); + await queryRunner.query(`DELETE FROM public.users`); + } +} diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index f13eea50..3977d46a 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -1,55 +1,26 @@ -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'; - -const mockOrdersRepository = mock>(); - -const mockPantry: Pantry = { - pantryId: 1, - pantryName: 'Test Pantry', - addressLine1: '123 Test St', - addressLine2: 'Apt. 1', - addressCity: 'Boston', - addressState: 'MA', - addressZip: '02115', - addressCountry: 'US', - allergenClients: '', - refrigeratedDonation: '', - reserveFoodForAllergic: 'Yes', - reservationExplanation: '', - dedicatedAllergyFriendly: '', - clientVisitFrequency: '', - identifyAllergensConfidence: '', - serveAllergicChildren: '', - newsletterSubscription: false, - restrictions: [], - pantryRepresentative: null as unknown as User, - status: 'active', - dateApplied: new Date(), - activities: [], - activitiesComments: '', - itemsInStock: '', - needMoreOptions: '', -}; - -describe('OrdersService', () => { +import { Order } from './order.entity'; +import { testDataSource } from '../config/typeormTestDataSource'; +import { CreateDummyData1759636753110 } from '../migrations/1759636753110-createDummyData'; + +describe('OrdersService (integration)', () => { let service: OrdersService; - let qb: SelectQueryBuilder; beforeAll(async () => { - mockOrdersRepository.createQueryBuilder.mockReset(); + // Create all tables and run migrations + if (!testDataSource.isInitialized) { + await testDataSource.initialize(); + await testDataSource.runMigrations(); + } - const module = await Test.createTestingModule({ + const module: TestingModule = await Test.createTestingModule({ providers: [ OrdersService, { provide: getRepositoryToken(Order), - useValue: mockOrdersRepository, + useValue: testDataSource.getRepository(Order), }, ], }).compile(); @@ -57,15 +28,41 @@ describe('OrdersService', () => { service = module.get(OrdersService); }); - beforeEach(() => { - qb = { - leftJoinAndSelect: jest.fn().mockReturnThis(), - select: jest.fn().mockReturnThis(), - andWhere: jest.fn().mockReturnThis(), - getMany: jest.fn().mockResolvedValue([]), - } as unknown as SelectQueryBuilder; + beforeEach(async () => { + const fkSafeOrder = [ + 'volunteer_assignments', + 'allocations', + 'orders', + 'food_requests', + 'donation_items', + 'donations', + 'pantries', + 'food_manufacturers', + 'users', + ]; + + // Delete all data, keep schema + for (const table of fkSafeOrder) { + await testDataSource.query( + `TRUNCATE TABLE "${table}" RESTART IDENTITY CASCADE`, + ); + } + + // Seed dummy data + const queryRunner = testDataSource.createQueryRunner(); + await queryRunner.connect(); + try { + await new CreateDummyData1759636753110().up(queryRunner); + } finally { + await queryRunner.release(); + } + }); - mockOrdersRepository.createQueryBuilder.mockReturnValue(qb); + afterAll(async () => { + // Destroy all schemas + if (testDataSource.isInitialized) { + await testDataSource.destroy(); + } }); it('should be defined', () => { @@ -73,117 +70,50 @@ describe('OrdersService', () => { }); describe('getAll', () => { - it('should return orders filtered by status', async () => { - const mockOrders: Partial[] = [ - { orderId: 1, status: 'pending' }, - { orderId: 2, status: 'delivered' }, - ]; + it('returns orders filtered by status', async () => { + const orders = await service.getAll({ status: 'delivered' }); - (qb.getMany as jest.Mock).mockResolvedValue([mockOrders[0] as Order]); - - const result = await service.getAll({ status: 'pending' }); - - expect(result).toEqual([mockOrders[0]]); - expect(qb.andWhere).toHaveBeenCalledWith('order.status = :status', { - status: 'pending', - }); + expect(orders).toHaveLength(3); + expect(orders.every((order) => order.status === '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' }); + it('returns empty array when status filter matches nothing', async () => { + const orders = await service.getAll({ status: 'invalid' }); - expect(result).toEqual([]); - expect(qb.andWhere).toHaveBeenCalledWith('order.status = :status', { - status: 'invalid status', - }); + expect(orders).toEqual([]); }); - it('should return orders filtered by pantryName', async () => { - const mockOrders: Partial[] = [ - { - orderId: 3, - status: 'delivered', - pantry: { ...mockPantry, pantryName: 'Test Pantry' }, - }, - { - orderId: 4, - status: 'delivered', - pantry: { ...mockPantry, pantryName: 'Test Pantry 2' }, - }, - { - orderId: 5, - status: 'delivered', - pantry: { ...mockPantry, pantryName: 'Test Pantry 3' }, - }, - ]; - - (qb.getMany as jest.Mock).mockResolvedValue( - mockOrders.slice(0, 2) as Order[], - ); - - const result = await service.getAll({ - pantryNames: ['Test Pantry', 'Test Pantry 2'], + it('returns orders filtered by pantry names', async () => { + const orders = await service.getAll({ + pantryNames: ['Community Food Pantry Downtown'], }); - expect(result).toEqual(mockOrders.slice(0, 2) as Order[]); - expect(qb.andWhere).toHaveBeenCalledWith( - 'pantry.pantryName IN (:...pantryNames)', - { pantryNames: ['Test Pantry', 'Test Pantry 2'] }, - ); + expect(orders).toHaveLength(3); + expect( + orders.every( + (order) => + order.pantry.pantryName === 'Community Food Pantry Downtown', + ), + ).toBe(true); }); - it('should return empty array when no pantryName filters match', async () => { - (qb.getMany as jest.Mock).mockResolvedValue([]); - - const result = await service.getAll({ + it('returns empty array when pantry filter matches nothing', async () => { + const orders = await service.getAll({ pantryNames: ['Nonexistent Pantry'], }); - expect(result).toEqual([]); - expect(qb.andWhere).toHaveBeenCalledWith( - 'pantry.pantryName IN (:...pantryNames)', - { pantryNames: ['Nonexistent Pantry'] }, - ); + expect(orders).toEqual([]); }); - it('should return orders filtered by both status and pantryName', async () => { - const mockOrders: Partial[] = [ - { - orderId: 3, - status: 'delivered', - pantry: { ...mockPantry, pantryName: 'Test Pantry 1' }, - }, - { - orderId: 4, - status: 'delivered', - pantry: { ...mockPantry, pantryName: 'Test Pantry 2' }, - }, - { - orderId: 5, - status: 'delivered', - pantry: { ...mockPantry, pantryName: 'Test Pantry 2' }, - }, - ]; - - (qb.getMany as jest.Mock).mockResolvedValue( - mockOrders.slice(1, 3) as Order[], - ); - - const result = await service.getAll({ + it('returns orders filtered by both pantry and status', async () => { + const orders = await service.getAll({ status: 'delivered', - pantryNames: ['Test Pantry 2'], + pantryNames: ['Westside Community Kitchen'], }); - 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).toHaveLength(1); + expect(orders[0].pantry.pantryName).toBe('Westside Community Kitchen'); + expect(orders[0].status).toBe('delivered'); }); }); }); diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index 10110223..7f571895 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -14,4 +14,4 @@ root.render( , -); \ No newline at end of file +); diff --git a/apps/frontend/src/theme.ts b/apps/frontend/src/theme.ts index c472e41f..c504bb84 100644 --- a/apps/frontend/src/theme.ts +++ b/apps/frontend/src/theme.ts @@ -1,4 +1,9 @@ -import { createSystem, defaultConfig, defineConfig, defineTextStyles } from '@chakra-ui/react'; +import { + createSystem, + defaultConfig, + defineConfig, + defineTextStyles, +} from '@chakra-ui/react'; const textStyles = defineTextStyles({ body: { @@ -10,7 +15,7 @@ const textStyles = defineTextStyles({ value: { fontFamily: 'instrument', fontSize: '32px', - fontWeight: '400' + fontWeight: '400', }, }, h2: { @@ -53,7 +58,7 @@ const customConfig = defineConfig({ colors: { white: { value: '#fff' }, black: { value: '#000' }, - blue: { + blue: { ssf: { value: '#2B5061' }, 100: { value: '#bee3f8' }, }, @@ -79,4 +84,4 @@ const customConfig = defineConfig({ }, }); -export const system = createSystem(defaultConfig, customConfig); \ No newline at end of file +export const system = createSystem(defaultConfig, customConfig);