From 8b7cbeb1dffa6b7e7064eedcd25b0c01123ce569 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Fri, 1 Nov 2024 15:54:57 -0700 Subject: [PATCH 01/12] added multi-select in the frontend for the orgUnit and category --- .../AdvancedSearchDropdown.scss | 4 + .../Openings/AdvancedSearchDropdown/index.tsx | 89 ++++++++++++------- .../Openings/OpeningsSearchBar/index.tsx | 11 ++- 3 files changed, 65 insertions(+), 39 deletions(-) diff --git a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/AdvancedSearchDropdown.scss b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/AdvancedSearchDropdown.scss index 5f40c7c6..15e8ef86 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/AdvancedSearchDropdown.scss +++ b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/AdvancedSearchDropdown.scss @@ -15,6 +15,10 @@ background-color: var(--bx-field-01); border-bottom: 1px solid var(--bx-border-strong-01); } + .multi-select .bx--list-box__field--wrapper{ + background-color: var(--bx-field-01); + border-bottom: 1px solid var(--bx-border-strong-01); + } } diff --git a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx index 3821b73e..5f664748 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx @@ -1,8 +1,9 @@ -import React, { useEffect } from "react"; +import React, { useState, useEffect } from "react"; import { Checkbox, CheckboxGroup, Dropdown, + MultiSelect, TextInput, FormLabel, Tooltip, @@ -17,7 +18,6 @@ import "./AdvancedSearchDropdown.scss"; import * as Icons from "@carbon/icons-react"; import { useOpeningFiltersQuery } from "../../../../services/queries/search/openingQueries"; import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; -import { color } from "@carbon/charts"; interface AdvancedSearchDropdownProps { toggleShowFilters: () => void; // Function to be passed as a prop @@ -28,11 +28,47 @@ const AdvancedSearchDropdown: React.FC = ({ const { filters, setFilters, clearFilters } = useOpeningsSearch(); const { data, isLoading, isError } = useOpeningFiltersQuery(); + // Initialize selected items for OrgUnit MultiSelect based on existing filters + const [selectedOrgUnits, setSelectedOrgUnits] = useState([]); + // Initialize selected items for category MultiSelect based on existing filters + const [selectedCategories, setSelectedCategories] = useState([]); + + + useEffect(() => { + // Split filters.orgUnit into array and format as needed for selectedItems + if (filters.orgUnit) { + const orgUnitsArray = filters.orgUnit.split(",").map((orgUnit: String) => ({ + text: orgUnit, + value: orgUnit, + })); + setSelectedOrgUnits(orgUnitsArray); + } + // Split filters.category into array and format as needed for selectedItems + if (filters.category) { + const categoriesArray = filters.category.split(",").map((category: String) => ({ + text: category, + value: category, + })); + setSelectedCategories(categoriesArray); + } + }, [filters.orgUnit, filters.category]); + const handleFilterChange = (updatedFilters: Partial) => { const newFilters = { ...filters, ...updatedFilters }; setFilters(newFilters); }; + const handleMultiSelectChange = (group: string, selectedItems: any) => { + const updatedGroup = selectedItems.map((item: any) => item.value); + //convert updatedGroup to string csv + const updatedGroupCsv = updatedGroup.join(","); + if (group === "orgUnit") + setSelectedOrgUnits(updatedGroup); + if (group === "category") + setSelectedCategories(updatedGroup); + handleFilterChange({ [group]: updatedGroupCsv }); + } + const handleCheckboxChange = (value: string, group: string) => { const selectedGroup = filters[group as keyof typeof filters] as string[]; const updatedGroup = selectedGroup.includes(value) @@ -119,41 +155,29 @@ const AdvancedSearchDropdown: React.FC = ({ - (item ? item.text : "")} - onChange={(e: any) => - handleFilterChange({ orgUnit: e.selectedItem.value }) - } + item.value === filters.orgUnit - ) - : "" - } + id="orgunit-multiselect" + className="multi-select" + titleText="Org Unit" + items={orgUnitItems} + itemToString={(item: any) => (item ? item.value : "")} + selectionFeedback="top-after-reopen" + onChange={(e: any) => handleMultiSelectChange("orgUnit", e.selectedItems)} + selectedItems={selectedOrgUnits} /> - (item ? item.text : "")} - onChange={(e: any) => - handleFilterChange({ category: e.selectedItem.value }) - } - label="Enter or choose a category" - selectedItem={ - filters.category - ? categoryItems.find( - (item: any) => item.value === filters.category - ) - : "" - } + itemToString={(item: any) => (item ? item.value : "")} + selectionFeedback="top-after-reopen" + onChange={(e: any) => handleMultiSelectChange("category", e.selectedItems)} + selectedItems={selectedCategories} /> @@ -168,7 +192,7 @@ const AdvancedSearchDropdown: React.FC = ({ label="If you don't remember the client information you can go to client search." > = ({ - = ({
{filtersCount > 0 ? ( - clearFilters()} > - {"+" + filtersCount} - + ) : null}

0 ? "text-active" : ""}> Advanced Search From 1a3d4763343f2895cb862b8494ff5ee4b6241b19 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Fri, 1 Nov 2024 16:11:53 -0700 Subject: [PATCH 02/12] fixed the clear and onload state persistence --- .../Openings/AdvancedSearchDropdown/index.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx index 5f664748..c21c94cc 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx @@ -18,6 +18,7 @@ import "./AdvancedSearchDropdown.scss"; import * as Icons from "@carbon/icons-react"; import { useOpeningFiltersQuery } from "../../../../services/queries/search/openingQueries"; import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; +import { Button } from "@carbon/react"; interface AdvancedSearchDropdownProps { toggleShowFilters: () => void; // Function to be passed as a prop @@ -42,6 +43,8 @@ const AdvancedSearchDropdown: React.FC = ({ value: orgUnit, })); setSelectedOrgUnits(orgUnitsArray); + } else { + setSelectedOrgUnits([]); } // Split filters.category into array and format as needed for selectedItems if (filters.category) { @@ -50,6 +53,8 @@ const AdvancedSearchDropdown: React.FC = ({ value: category, })); setSelectedCategories(categoriesArray); + } else{ + setSelectedCategories([]); } }, [filters.orgUnit, filters.category]); From edb7eedd213719baa843fa54620fb1b90ec2f8f4 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Mon, 4 Nov 2024 10:17:16 -0800 Subject: [PATCH 03/12] changed the MulitSelect to Filterable MultiSelect --- .../Openings/AdvancedSearchDropdown/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx index c21c94cc..abb0f037 100644 --- a/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx +++ b/frontend/src/components/SilvicultureSearch/Openings/AdvancedSearchDropdown/index.tsx @@ -3,7 +3,6 @@ import { Checkbox, CheckboxGroup, Dropdown, - MultiSelect, TextInput, FormLabel, Tooltip, @@ -19,6 +18,7 @@ import * as Icons from "@carbon/icons-react"; import { useOpeningFiltersQuery } from "../../../../services/queries/search/openingQueries"; import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; import { Button } from "@carbon/react"; +import { FilterableMultiSelect } from "@carbon/react"; interface AdvancedSearchDropdownProps { toggleShowFilters: () => void; // Function to be passed as a prop @@ -160,7 +160,7 @@ const AdvancedSearchDropdown: React.FC = ({ - = ({ /> - Date: Mon, 4 Nov 2024 11:21:52 -0800 Subject: [PATCH 04/12] fix(SILVA-546): fixing amplify authentication (#441) --- .github/workflows/analysis.yml | 5 + .github/workflows/merge.yml | 2 + .github/workflows/pr-open.yml | 1 + common/openshift.init.yml | 4 + frontend/Caddyfile | 2 +- frontend/openshift.deploy.yml | 5 + frontend/package-lock.json | 618 +----------------- frontend/package.json | 5 +- frontend/src/App.tsx | 81 +-- .../src/__test__/actions/userAction.test.ts | 86 +++ .../components/BCHeaderwSide.test.tsx | 17 +- .../__test__/components/MyProfile.test.tsx | 85 +++ .../__test__/contexts/AuthProvider.test.tsx | 173 +++++ frontend/src/__test__/index.test.tsx | 53 ++ .../__test__/routes/ProtectedRoute.test.tsx | 75 +++ .../src/__test__/screens/Dashboard.test.tsx | 39 -- .../screens/DashboardRedirect.test.tsx | 51 ++ .../__test__/screens/ErrorHandling.test.tsx | 83 +++ .../src/__test__/screens/Landing.test.tsx | 84 +++ .../src/__test__/services/AuthService.test.ts | 130 ++++ frontend/src/__test__/utils/famUtils.test.ts | 76 +++ frontend/src/actions/userAction.ts | 8 +- frontend/src/amplifyconfiguration.ts | 4 +- frontend/src/components/MyProfile/index.tsx | 14 +- .../OrganizationSelection/index.tsx | 231 +------ frontend/src/contexts/AuthProvider.tsx | 133 ++++ frontend/src/index.tsx | 30 +- frontend/src/routes/PostLoginRoute.tsx | 44 -- frontend/src/routes/ProtectedRoute.tsx | 54 +- frontend/src/screens/Dashboard/Dashboard.scss | 4 - frontend/src/screens/Dashboard/index.tsx | 23 - .../src/screens/DashboardRedirect/index.tsx | 29 +- frontend/src/screens/ErrorHandling/index.tsx | 23 + frontend/src/screens/Landing/index.tsx | 32 +- frontend/src/screens/Opening/index.tsx | 13 +- frontend/src/services/AuthService.ts | 173 +---- frontend/src/store.ts | 1 + frontend/src/types/amplify.ts | 4 + frontend/src/utils/famUtils.ts | 21 +- 39 files changed, 1286 insertions(+), 1230 deletions(-) create mode 100644 frontend/src/__test__/actions/userAction.test.ts create mode 100644 frontend/src/__test__/components/MyProfile.test.tsx create mode 100644 frontend/src/__test__/contexts/AuthProvider.test.tsx create mode 100644 frontend/src/__test__/index.test.tsx create mode 100644 frontend/src/__test__/routes/ProtectedRoute.test.tsx delete mode 100644 frontend/src/__test__/screens/Dashboard.test.tsx create mode 100644 frontend/src/__test__/screens/DashboardRedirect.test.tsx create mode 100644 frontend/src/__test__/screens/ErrorHandling.test.tsx create mode 100644 frontend/src/__test__/screens/Landing.test.tsx create mode 100644 frontend/src/__test__/services/AuthService.test.ts create mode 100644 frontend/src/__test__/utils/famUtils.test.ts create mode 100644 frontend/src/contexts/AuthProvider.tsx delete mode 100644 frontend/src/routes/PostLoginRoute.tsx delete mode 100644 frontend/src/screens/Dashboard/Dashboard.scss delete mode 100644 frontend/src/screens/Dashboard/index.tsx create mode 100644 frontend/src/screens/ErrorHandling/index.tsx create mode 100644 frontend/src/types/amplify.ts diff --git a/.github/workflows/analysis.yml b/.github/workflows/analysis.yml index bf364cb9..0a79ef2c 100644 --- a/.github/workflows/analysis.yml +++ b/.github/workflows/analysis.yml @@ -55,6 +55,11 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: bcgov-nr/action-test-and-analyse@v1.2.1 + env: + VITE_ZONE: test + VITE_USER_POOLS_ID: ca-central-1_abc123 + VITE_USER_POOLS_WEB_CLIENT_ID: abc123 + VITE_BACKEND_URL: http://localhost:8080 with: commands: | npm ci diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index 8d4a8bd4..b0f39c68 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -34,6 +34,7 @@ jobs: -p POSTGRES_DB_PASSWORD='${{ secrets.POSTGRES_DB_PASSWORD }}' -p FORESTCLIENTAPI_KEY='${{ secrets.FORESTCLIENTAPI_KEY }}' -p VITE_USER_POOLS_WEB_CLIENT_ID=${{ vars.VITE_USER_POOLS_WEB_CLIENT_ID }} + -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }} -p ZONE=test deploys-test: @@ -106,6 +107,7 @@ jobs: -p POSTGRES_DB_PASSWORD='${{ secrets.POSTGRES_DB_PASSWORD }}' -p FORESTCLIENTAPI_KEY='${{ secrets.FORESTCLIENTAPI_KEY }}' -p VITE_USER_POOLS_WEB_CLIENT_ID=${{ vars.VITE_USER_POOLS_WEB_CLIENT_ID }} + -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }} -p ZONE=prod image-promotions: diff --git a/.github/workflows/pr-open.yml b/.github/workflows/pr-open.yml index 6a009bf0..b7ccb11d 100644 --- a/.github/workflows/pr-open.yml +++ b/.github/workflows/pr-open.yml @@ -65,6 +65,7 @@ jobs: -p POSTGRES_DB_PASSWORD='${{ secrets.POSTGRES_DB_PASSWORD }}' -p FORESTCLIENTAPI_KEY='${{ secrets.FORESTCLIENTAPI_KEY }}' -p VITE_USER_POOLS_WEB_CLIENT_ID=${{ vars.VITE_USER_POOLS_WEB_CLIENT_ID }} + -p VITE_USER_POOLS_ID=${{ vars.VITE_USER_POOLS_ID }} triggers: ('common/' 'backend/' 'frontend/') builds: diff --git a/common/openshift.init.yml b/common/openshift.init.yml index f6f83b4b..1249a65e 100644 --- a/common/openshift.init.yml +++ b/common/openshift.init.yml @@ -40,6 +40,9 @@ parameters: - name: VITE_USER_POOLS_WEB_CLIENT_ID description: Cognito user pools web client ID required: true + - name: VITE_USER_POOLS_ID + description: Cognito user pools ID + required: true objects: - apiVersion: v1 kind: Secret @@ -95,6 +98,7 @@ objects: app: ${NAME}-${ZONE} stringData: vite-user-pools-web-client-id: ${VITE_USER_POOLS_WEB_CLIENT_ID} + vite-user-pools-id: ${VITE_USER_POOLS_ID} - apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: diff --git a/frontend/Caddyfile b/frontend/Caddyfile index a3a8ed2c..21770316 100644 --- a/frontend/Caddyfile +++ b/frontend/Caddyfile @@ -28,7 +28,7 @@ } handle /env.js { header Content-Type "text/javascript" - respond `window.config = {"VITE_USER_POOLS_WEB_CLIENT_ID":"{$VITE_USER_POOLS_WEB_CLIENT_ID}","VITE_ZONE":"{$VITE_ZONE}","VITE_BACKEND_URL":"{$VITE_BACKEND_URL}"};` + respond `window.config = {"VITE_USER_POOLS_ID":"{$VITE_USER_POOLS_ID}","VITE_USER_POOLS_WEB_CLIENT_ID":"{$VITE_USER_POOLS_WEB_CLIENT_ID}","VITE_ZONE":"{$VITE_ZONE}","VITE_BACKEND_URL":"{$VITE_BACKEND_URL}"};` } handle_path /* { diff --git a/frontend/openshift.deploy.yml b/frontend/openshift.deploy.yml index f20f6f6c..34ed6e50 100644 --- a/frontend/openshift.deploy.yml +++ b/frontend/openshift.deploy.yml @@ -91,6 +91,11 @@ objects: secretKeyRef: name: ${NAME}-${ZONE}-frontend key: vite-user-pools-web-client-id + - name: VITE_USER_POOLS_ID + valueFrom: + secretKeyRef: + name: ${NAME}-${ZONE}-frontend + key: vite-user-pools-id ports: - containerPort: 3000 protocol: TCP diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7191f4e7..597bbb91 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,8 +18,7 @@ "@types/node": "^22.0.0", "@vitejs/plugin-react": "^4.0.4", "@vitejs/plugin-react-swc": "^3.3.2", - "amazon-cognito-identity-js": "^6.3.13", - "aws-amplify": "^6.5.0", + "aws-amplify": "^6.7.0", "axios": "^1.6.8", "jspdf": "^2.5.2", "jspdf-autotable": "^3.8.3", @@ -134,31 +133,6 @@ "uuid": "^9.0.0" } }, - "node_modules/@aws-amplify/api-graphql/node_modules/@aws-sdk/types": { - "version": "3.387.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.387.0.tgz", - "integrity": "sha512-YTjFabNwjTF+6yl88f0/tWff018qmmgMmjlw45s6sdVKueWxdxV68U7gepNLF2nhaQPZa6FDOBoA51NaviVs0Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^2.1.0", - "tslib": "^2.5.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-amplify/api-graphql/node_modules/@smithy/types": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", - "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-amplify/api-rest": { "version": "4.0.56", "resolved": "https://registry.npmjs.org/@aws-amplify/api-rest/-/api-rest-4.0.56.tgz", @@ -199,31 +173,6 @@ "uuid": "^9.0.0" } }, - "node_modules/@aws-amplify/core/node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-amplify/core/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, "node_modules/@aws-amplify/core/node_modules/@aws-sdk/types": { "version": "3.398.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.398.0.tgz", @@ -249,15 +198,6 @@ "node": ">=14.0.0" } }, - "node_modules/@aws-amplify/core/node_modules/js-cookie": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", - "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@aws-amplify/data-schema": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@aws-amplify/data-schema/-/data-schema-1.12.1.tgz", @@ -366,17 +306,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-crypto/crc32/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -392,7 +321,7 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@aws-crypto/sha256-js": { + "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", @@ -406,34 +335,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz", - "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^1.2.2", - "@aws-sdk/types": "^3.1.0", - "tslib": "^1.11.1" - } - }, - "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, "node_modules/@aws-crypto/supports-web-crypto": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", @@ -444,22 +345,16 @@ } }, "node_modules/@aws-crypto/util": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz", - "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.1.0", - "@aws-sdk/util-utf8-browser": "^3.0.0", - "tslib": "^1.11.1" + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/util/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, "node_modules/@aws-sdk/client-firehose": { "version": "3.621.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-firehose/-/client-firehose-3.621.0.tgz", @@ -512,57 +407,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-firehose/node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-firehose/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-firehose/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-firehose/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-firehose/node_modules/@aws-sdk/types": { "version": "3.609.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", @@ -576,18 +420,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-firehose/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-firehose/node_modules/@smithy/util-utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", @@ -657,57 +489,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-kinesis/node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-kinesis/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-kinesis/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-kinesis/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-kinesis/node_modules/@aws-sdk/types": { "version": "3.609.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", @@ -721,18 +502,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-kinesis/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-kinesis/node_modules/@smithy/util-utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", @@ -798,57 +567,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-personalize-events/node_modules/@aws-sdk/types": { "version": "3.609.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", @@ -862,18 +580,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-personalize-events/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-personalize-events/node_modules/@smithy/util-utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", @@ -989,57 +695,6 @@ "@aws-sdk/client-sts": "^3.621.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-sso-oidc/node_modules/@aws-sdk/types": { "version": "3.609.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", @@ -1053,18 +708,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-sso-oidc/node_modules/@smithy/util-utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", @@ -1078,57 +721,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sso/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-sso/node_modules/@aws-sdk/types": { "version": "3.609.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", @@ -1142,18 +734,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sso/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-sso/node_modules/@smithy/util-utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", @@ -1218,57 +798,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-sdk/client-sts/node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/types": { "version": "3.609.0", "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.609.0.tgz", @@ -1282,18 +811,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/client-sts/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-sts/node_modules/@smithy/util-utf8": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz", @@ -1750,16 +1267,28 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.679.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.679.0.tgz", - "integrity": "sha512-NwVq8YvInxQdJ47+zz4fH3BRRLC6lL+WLkvr242PVBbUOLRyK/lkwHlfiKUoeVIMyK5NF+up6TRg71t/8Bny6Q==", + "version": "3.387.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.387.0.tgz", + "integrity": "sha512-YTjFabNwjTF+6yl88f0/tWff018qmmgMmjlw45s6sdVKueWxdxV68U7gepNLF2nhaQPZa6FDOBoA51NaviVs0Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^2.1.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/types/node_modules/@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^3.5.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=16.0.0" + "node": ">=14.0.0" } }, "node_modules/@aws-sdk/util-endpoints": { @@ -1863,15 +1392,6 @@ "node": ">=16.0.0" } }, - "node_modules/@aws-sdk/util-utf8-browser": { - "version": "3.259.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", - "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.3.1" - } - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -6285,19 +5805,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/amazon-cognito-identity-js": { - "version": "6.3.14", - "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.14.tgz", - "integrity": "sha512-nxN8L5AAwLIsgQKyKMOsNwr5xeY7+fccv+A/ALiYxmGiM341XX0dcoMuM+LlJmzfIfuPmTrXSehhTunTTQFAow==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "1.2.2", - "buffer": "4.9.2", - "fast-base64-decode": "^1.0.0", - "isomorphic-unfetch": "^3.0.0", - "js-cookie": "^2.2.1" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -9005,12 +8512,6 @@ "node": ">=12.0.0" } }, - "node_modules/fast-base64-decode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", - "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==", - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -10258,16 +9759,6 @@ "dev": true, "license": "ISC" }, - "node_modules/isomorphic-unfetch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", - "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", - "license": "MIT", - "dependencies": { - "node-fetch": "^2.6.1", - "unfetch": "^4.2.0" - } - }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -10521,10 +10012,13 @@ } }, "node_modules/js-cookie": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", - "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", - "license": "MIT" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } }, "node_modules/js-tokens": { "version": "4.0.0", @@ -11123,48 +10617,6 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/node-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/node-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -13252,12 +12704,6 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "license": "MIT" }, - "node_modules/unfetch": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", - "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", - "license": "MIT" - }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8e667992..355b73bc 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,9 +13,8 @@ "@tanstack/react-query": "^5.50.1", "@types/node": "^22.0.0", "@vitejs/plugin-react": "^4.0.4", - "@vitejs/plugin-react-swc": "^3.3.2", - "amazon-cognito-identity-js": "^6.3.13", - "aws-amplify": "^6.5.0", + "@vitejs/plugin-react-swc": "^3.3.2", + "aws-amplify": "^6.7.0", "axios": "^1.6.8", "jspdf": "^2.5.2", "jspdf-autotable": "^3.8.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2d44cba2..79bc4953 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,52 +1,59 @@ -import { - BrowserRouter, Routes, Route -} from 'react-router-dom'; -import { Amplify } from 'aws-amplify'; -import amplifyconfig from './amplifyconfiguration'; - +import { createBrowserRouter, RouterProvider } from 'react-router-dom'; import './custom.scss'; - import Landing from "./screens/Landing"; import Help from "./screens/Help"; import Reports from './screens/Reports'; import SideLayout from './layouts/SideLayout'; -import PostLoginRoute from './routes/PostLoginRoute'; import ProtectedRoute from './routes/ProtectedRoute'; import Opening from './screens/Opening'; import DashboardRedirect from './screens/DashboardRedirect'; import SilvicultureSearch from './screens/SilvicultureSearch'; +import ErrorHandling from './screens/ErrorHandling'; + -Amplify.configure(amplifyconfig); +// Create the router instance +const router = createBrowserRouter([ + { + path: "/", + element: , + errorElement: // Handle errors for the Landing page route + }, + { + path: "/dashboard", + element: , + errorElement: // Handle errors for the dashboard route + }, + { + element: , + errorElement: , // Global error element for protected routes + children: [ + { + path: "/opening", + element: } /> + }, + { + path: "/silviculture-search", + element: } /> + }, + { + path: "/opening/reports", + element: } /> + }, + { + path: "/help", + element: } /> + } + ] + }, + // Catch-all route for unmatched paths + { + path: "*", + element: + } +]); const App: React.FC = () => { - return ( - - - } /> - - - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - - } /> - } />} /> - - - ); + return ; }; export default App; diff --git a/frontend/src/__test__/actions/userAction.test.ts b/frontend/src/__test__/actions/userAction.test.ts new file mode 100644 index 00000000..592f409f --- /dev/null +++ b/frontend/src/__test__/actions/userAction.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect, vi } from 'vitest'; +import { getUserDetails, setClientRoles } from '../../actions/userAction'; +import { + USER_DETAILS_REQUEST, +USER_DETAILS_SUCCESS, +USER_DETAILS_FAIL, +SET_CLIENT_ROLES +} from '../../constants/userConstants'; +import { useGetAuth } from '../../contexts/AuthProvider'; +import { AppDispatch } from '../../store'; +import { UserClientRolesType } from '../../types/UserRoleType'; + + + +vi.mock('../../contexts/AuthProvider', () => ({ +useGetAuth: vi.fn(), +})); + +describe('userAction', () => { +describe('getUserDetails', () => { + it('should dispatch USER_DETAILS_REQUEST and USER_DETAILS_SUCCESS with user data when successful', async () => { + const mockDispatch = vi.fn(); + const mockIsLoggedIn = true; + const mockUser = { firstName: 'John', lastName: 'Doe' }; + const mockUserJSON = JSON.stringify(mockUser); + + (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: mockIsLoggedIn }); + localStorage.setItem('famLoginUser', mockUserJSON); + + await getUserDetails()(mockDispatch as unknown as AppDispatch); + + expect(mockDispatch).toHaveBeenCalledWith({ type: USER_DETAILS_REQUEST }); + expect(mockDispatch).toHaveBeenCalledWith({ + type: USER_DETAILS_SUCCESS, + payload: { ...mockUser, isLoggedIn: mockIsLoggedIn }, + }); + }); + + it('should dispatch USER_DETAILS_FAIL with error when an error occurs', async () => { + const mockDispatch = vi.fn(); + const mockError = new Error('Test error'); + + (useGetAuth as vi.Mock).mockImplementation(() => { + throw mockError; + }); + + await getUserDetails()(mockDispatch as unknown as AppDispatch); + + expect(mockDispatch).toHaveBeenCalledWith({ type: USER_DETAILS_REQUEST }); + expect(mockDispatch).toHaveBeenCalledWith({ + type: USER_DETAILS_FAIL, + payload: { error: mockError }, + }); + }); + + it('should handle missing user data in localStorage', async () => { + const mockDispatch = vi.fn(); + const mockIsLoggedIn = true; + + (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: mockIsLoggedIn }); + localStorage.removeItem('famLoginUser'); + + await getUserDetails()(mockDispatch as unknown as AppDispatch); + + expect(mockDispatch).toHaveBeenCalledWith({ type: USER_DETAILS_REQUEST }); + expect(mockDispatch).toHaveBeenCalledWith({ + type: USER_DETAILS_SUCCESS, + payload: { isLoggedIn: mockIsLoggedIn }, + }); + }); +}); + +describe('setClientRoles', () => { + it('should dispatch SET_CLIENT_ROLES with client roles', () => { + const mockDispatch = vi.fn(); + const mockClientRoles: UserClientRolesType[] = [{ clientId: '123', roles: ['admin'] }]; + + setClientRoles(mockClientRoles)(mockDispatch as unknown as AppDispatch); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: SET_CLIENT_ROLES, + payload: mockClientRoles, + }); + }); +}); +}); \ No newline at end of file diff --git a/frontend/src/__test__/components/BCHeaderwSide.test.tsx b/frontend/src/__test__/components/BCHeaderwSide.test.tsx index f3aa1fea..c3a49c78 100644 --- a/frontend/src/__test__/components/BCHeaderwSide.test.tsx +++ b/frontend/src/__test__/components/BCHeaderwSide.test.tsx @@ -10,6 +10,7 @@ import { Provider } from 'react-redux'; import store from '../../store'; import { UserClientRolesType } from '../../types/UserRoleType'; import '@testing-library/jest-dom'; +import { AuthProvider } from '../../contexts/AuthProvider'; vi.mock('../../services/TestService', () => ({ getForestClientByNumberOrAcronym: vi.fn(() => [ @@ -29,13 +30,15 @@ const renderComponent = () => { const qc = new QueryClient(); render( - - - - - - - + + + + + + + + + ); }; diff --git a/frontend/src/__test__/components/MyProfile.test.tsx b/frontend/src/__test__/components/MyProfile.test.tsx new file mode 100644 index 00000000..5e723ce3 --- /dev/null +++ b/frontend/src/__test__/components/MyProfile.test.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import React from 'react'; +import { render, act, waitFor, fireEvent, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import MyProfile from '../../components/MyProfile'; +import { useThemePreference } from '../../utils/ThemePreference'; +import { useGetAuth } from '../../contexts/AuthProvider'; + +// Mock dependencies +vi.mock('../../utils/ThemePreference', () => ({ + useThemePreference: vi.fn(), +})); + +vi.mock('../../contexts/AuthProvider', () => ({ + useGetAuth: vi.fn(), +})); + +describe('MyProfile Component', () => { + const mockSetTheme = vi.fn(); + const mockLogout = vi.fn(); + const mockAuthUser = { + firstName: 'John', + lastName: 'Doe', + userName: 'johndoe', + email: 'john.doe@example.com', + }; + + beforeEach(() => { + (useThemePreference as vi.Mock).mockReturnValue({ + theme: 'g10', + setTheme: mockSetTheme, + }); + (useGetAuth as vi.Mock).mockReturnValue({ + logout: mockLogout, + user: mockAuthUser, + }); + + // Mock localStorage + vi.spyOn(Storage.prototype, 'setItem'); + }); + + it('should render user information correctly', () => { + render(); + expect(screen.getByText('John Doe')).toBeDefined(); + expect(screen.getByText('IDIR: johndoe')).toBeDefined(); + expect(screen.getByText('Email:john.doe@example.com')).toBeDefined(); + }); + + it('should change theme when "Change theme" button is clicked', () => { + render(); + const changeThemeButton = screen.getByText('Change theme'); + fireEvent.click(changeThemeButton); + expect(mockSetTheme).toHaveBeenCalledWith('g100'); + expect(localStorage.setItem).toHaveBeenCalledWith('mode', 'dark'); + }); + + it('should call logout function when "Log out" button is clicked', () => { + render(); + const logoutButton = screen.getByText('Log out'); + fireEvent.click(logoutButton); + expect(mockLogout).toHaveBeenCalled(); + }); + + it('should render organization selection section', () => { + render(); + expect(screen.getByText('Select organization')).toBeDefined(); + }); + + it('should render options section', () => { + render(); + expect(screen.getByText('Options')).toBeDefined(); + }); + + it('should toggle theme between light and dark', () => { + (useThemePreference as vi.Mock).mockReturnValueOnce({ + theme: 'g100', + setTheme: mockSetTheme, + }); + render(); + const changeThemeButton = screen.getByText('Change theme'); + fireEvent.click(changeThemeButton); + expect(mockSetTheme).toHaveBeenCalledWith('g10'); + expect(localStorage.setItem).toHaveBeenCalledWith('mode', 'light'); + }); +}); diff --git a/frontend/src/__test__/contexts/AuthProvider.test.tsx b/frontend/src/__test__/contexts/AuthProvider.test.tsx new file mode 100644 index 00000000..3430ccca --- /dev/null +++ b/frontend/src/__test__/contexts/AuthProvider.test.tsx @@ -0,0 +1,173 @@ +import React from 'react'; +import { render, act, waitFor } from '@testing-library/react'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { AuthProvider, useGetAuth } from '../../contexts/AuthProvider'; +import { signInWithRedirect, signOut } from 'aws-amplify/auth'; +import { parseToken } from '../../services/AuthService'; +import { extractGroups } from '../../utils/famUtils'; +import { env } from '../../env'; + +vi.mock('aws-amplify/auth', () => ({ + signInWithRedirect: vi.fn(), + signOut: vi.fn(), +})); + +function setAuthCookies(value: string | null) { + const cookieName = `CognitoIdentityServiceProvider.${env.VITE_USER_POOLS_WEB_CLIENT_ID}`; + document.cookie = `${cookieName}.LastAuthUser=; path=/; max-age=0`; + document.cookie = `${cookieName}.abci21.idToken=; path=/; max-age=0`; + + if (value) { + document.cookie = `${cookieName}.LastAuthUser=abci21; path=/`; + document.cookie = `${cookieName}.abci21.idToken=${value}; path=/`; + } +} + +const sampleAuthToken = 'eyJhbGciOiJIUzI1NiJ9.eyJjb2duaXRvOmdyb3VwcyI6WyJncm91cDEiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYjVlY2RiMDk0ZGZiNDE0OWE2YTg0NDVhMDFhOTZiZjBAaWRpciIsImN1c3RvbTppZHBfdXNlcl9pZCI6IkI1RUNEQjA5NERGQjQxNDlBNkE4NDQ1QTAxQTk2QkYwIiwiY3VzdG9tOmlkcF91c2VybmFtZSI6IkpSWUFOIiwiY3VzdG9tOmlkcF9kaXNwbGF5X25hbWUiOiJSeWFuLCBKYWNrIENJQTpJTiIsImVtYWlsIjoiamFjay5yeWFuQGdvdi5iYy5jYSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiY3VzdG9tOmlkcF9uYW1lIjoiaWRpciIsImdpdmVuX25hbWUiOiJKYWNrIiwibmFtZSI6IkphY2sgUnlhbiIsImZhbWlseV9uYW1lIjoiUnlhbiJ9.cLEC8Yh08HErgP2x33pgt2koYJlFNRfi7ja7etcabrM'; + +describe('AuthProvider', () => { + const mockUser = { firstName: 'John', lastName: 'Doe' }; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should initialize correctly', async () => { + setAuthCookies(sampleAuthToken); + + const TestComponent = () => { + const { user, isLoggedIn, isLoading } = useGetAuth(); + return ( +

+ {isLoading ? 'Loading' : 'Loaded'} + {isLoggedIn ? 'Logged In' : 'Logged Out'} + {user?.firstName} +
+ ); + }; + + let getByText; + await act(async () => { + ({ getByText } = render( + + + + )); + }); + + await waitFor(() => expect(getByText('Loaded')).toBeDefined()); + expect(getByText('Logged In')).toBeDefined(); + expect(getByText('Jack')).toBeDefined(); + }); + + it('should find no token', async () => { + setAuthCookies(null); + + const TestComponent = () => { + const { user, isLoggedIn, isLoading } = useGetAuth(); + return ( +
+ {isLoading ? 'Loading' : 'Loaded'} + {isLoggedIn ? 'Logged In' : 'Logged Out'} + Welcome {user?.firstName || 'nobody'} +
+ ); + }; + + let getByText; + await act(async () => { + ({ getByText } = render( + + + + )); + }); + + await waitFor(() => expect(getByText('Loaded')).toBeDefined()); + expect(getByText('Logged Out')).toBeDefined(); + expect(getByText('Welcome nobody')).toBeDefined(); + }); + + it('should handle login correctly', async () => { + setAuthCookies(sampleAuthToken); + const provider = 'idir'; + const envProvider = 'TEST-IDIR'; + + const TestComponent = () => { + const { login } = useGetAuth(); + return ; + }; + + let getByText; + await act(async () => { + ({ getByText } = render( + + + + )); + }); + + act(() => { + getByText('Login').click(); + }); + + expect(signInWithRedirect).toHaveBeenCalledWith({ + provider: { custom: envProvider.toUpperCase() }, + }); + }); + + it('should handle logout correctly', async () => { + setAuthCookies(sampleAuthToken); + + const TestComponent = () => { + const { logout } = useGetAuth(); + return ; + }; + + let getByText; + await act(async () => { + ({ getByText } = render( + + + + )); + }); + + act(() => { + getByText('Logout').click(); + }); + + await waitFor(() => expect(signOut).toHaveBeenCalled()); + }); + + it('should handle userDetails correctly', async () => { + setAuthCookies(sampleAuthToken); + + const TestComponent = () => { + const { user } = useGetAuth(); + return
{user ? user.firstName : 'No User'}
; + }; + + let getByText; + await act(async () => { + ({ getByText } = render( + + + + )); + }); + + await waitFor(() => expect(getByText('Jack')).toBeDefined()); + }); + + it('should throw error if useGetAuth is used outside AuthProvider', () => { + const TestComponent = () => { + useGetAuth(); + return
Test
; + }; + + expect(() => render()).toThrow( + 'useGetAuth must be used within an AuthProvider' + ); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/index.test.tsx b/frontend/src/__test__/index.test.tsx new file mode 100644 index 00000000..dba03fcd --- /dev/null +++ b/frontend/src/__test__/index.test.tsx @@ -0,0 +1,53 @@ +import { describe, it, expect, vi } from 'vitest'; +import React from 'react'; +import { render } from '@testing-library/react'; +import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Provider } from 'react-redux'; +import { Amplify } from 'aws-amplify'; +import App from '../App'; +import store from '../store'; +import { ThemePreference } from '../utils/ThemePreference'; +import PaginationProvider from '../contexts/PaginationProvider'; +import { OpeningsSearchProvider } from '../contexts/search/OpeningsSearch'; +import { AuthProvider } from '../contexts/AuthProvider'; +import amplifyconfig from '../amplifyconfiguration'; +import { CookieStorage } from 'aws-amplify/utils'; +import { cognitoUserPoolsTokenProvider } from 'aws-amplify/auth/cognito'; +import { useLottie } from 'lottie-react'; + +vi.mock('aws-amplify'); +vi.mock('aws-amplify/utils'); +vi.mock('aws-amplify/auth/cognito'); +vi.mock('@tanstack/react-query', () => ({ + QueryClient: vi.fn(() => ({ + defaultOptions: { + queries: {}, + }, + })), + QueryClientProvider: ({ children }) =>
{children}
, +})); +vi.mock('react-dom/client', () => ({ + createRoot: vi.fn(() => ({ + render: vi.fn(), + })), +})); +vi.mock('lottie-react', () => ({ + useLottie: vi.fn(), +})); + +describe('index.tsx', () => { + it('should initialize the app correctly', async () => { + (useLottie as vi.Mock).mockReturnValue({ View:
Lottie Animation
}); + const container = document.createElement('div'); + container.id = 'root'; + document.body.appendChild(container); + + // Import the index file to execute its code + await import('../index'); + + expect(createRoot).toHaveBeenCalledWith(container); + expect(Amplify.configure).toHaveBeenCalledWith(amplifyconfig); + expect(cognitoUserPoolsTokenProvider.setKeyValueStorage).toHaveBeenCalledWith(expect.any(CookieStorage)); + }); +}); diff --git a/frontend/src/__test__/routes/ProtectedRoute.test.tsx b/frontend/src/__test__/routes/ProtectedRoute.test.tsx new file mode 100644 index 00000000..3783f05b --- /dev/null +++ b/frontend/src/__test__/routes/ProtectedRoute.test.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { describe, it, expect, vi } from 'vitest'; +import ProtectedRoute from '../../routes/ProtectedRoute'; +import { useGetAuth } from '../../contexts/AuthProvider'; +import { Loading } from "@carbon/react"; + +vi.mock('../../contexts/AuthProvider', () => ({ + useGetAuth: vi.fn(), +})); + +vi.mock('@carbon/react', () => ({ + Loading: vi.fn(() =>
Loading...
), +})); + +describe('ProtectedRoute', () => { + it('should render loading component when isLoading is true', () => { + (useGetAuth as vi.Mock).mockReturnValue({ isLoading: true }); + + const { getByText } = render( + + + + ); + + expect(getByText('Loading...')).toBeDefined(); + }); + + it('should redirect to login when requireAuth is true and user is not logged in', () => { + (useGetAuth as vi.Mock).mockReturnValue({ isLoading: false, isLoggedIn: false }); + + const { container } = render( + + + Login Page
} /> + } /> + + + ); + + expect(container.innerHTML).toContain('Login Page'); + }); + + it('should redirect to unauthorized when requiredRoles are not met', () => { + (useGetAuth as vi.Mock).mockReturnValue({ isLoading: false, isLoggedIn: true, userRoles: ['user'] }); + + const { container } = render( + + + Unauthorized Page} /> + } /> + + + ); + + expect(container.innerHTML).toContain('Unauthorized Page'); + }); + + it('should render child routes when all checks pass', () => { + (useGetAuth as vi.Mock).mockReturnValue({ isLoading: false, isLoggedIn: true, userRoles: ['admin'] }); + + const { container } = render( + + + }> + Protected Content} /> + + + + ); + + expect(container.innerHTML).toContain('Protected Content'); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/screens/Dashboard.test.tsx b/frontend/src/__test__/screens/Dashboard.test.tsx deleted file mode 100644 index bcf0e9b9..00000000 --- a/frontend/src/__test__/screens/Dashboard.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import { describe, expect, it, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import { Provider } from 'react-redux'; -import store from '../../store'; -import Dashboard from '../../screens/Dashboard'; -import * as redux from 'react-redux'; - -const state = { - userDetails: { - user: { - firstName: 'Test' - }, - loading: false, - error: null - }, - selectedClientRoles: { - - } -}; - -vi.spyOn(redux, 'useSelector') - .mockImplementation((callback) => callback(state)); - -describe('Dashboard screen test cases', () => { - it('should render the dashboard component', () => { - render( - - - - - - ); - - const headerElement = screen.getByText(/welcome to the SILVA/i); - expect(headerElement).toBeDefined(); - }) -}); diff --git a/frontend/src/__test__/screens/DashboardRedirect.test.tsx b/frontend/src/__test__/screens/DashboardRedirect.test.tsx new file mode 100644 index 00000000..14d032e7 --- /dev/null +++ b/frontend/src/__test__/screens/DashboardRedirect.test.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { MemoryRouter, Navigate } from 'react-router-dom'; +import { describe, it, expect, vi } from 'vitest'; +import DashboardRedirect from '../../screens/DashboardRedirect'; +import { useGetAuth } from '../../contexts/AuthProvider'; + +vi.mock('../../contexts/AuthProvider', () => ({ + useGetAuth: vi.fn(), +})); + +vi.mock('@carbon/react', () => ({ + Loading: vi.fn(() =>
Loading...
), +})); + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + Navigate: vi.fn(({ to, replace }) =>
Navigate to {to} - {replace}
), + }; +}); + +describe('DashboardRedirect', () => { + it('should render Loading component when user is not defined', () => { + (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: false }); + + const { getByText } = render( + + + + ); + + expect(getByText('Loading...')).toBeDefined(); + expect(Navigate).not.toHaveBeenCalled(); + }); + + it('should navigate to /opening when user is defined', async () => { + (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: true }); + + const { getByText } = render( + + + + ); + + await waitFor(() => { + expect(getByText('Navigate to /opening -')).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/screens/ErrorHandling.test.tsx b/frontend/src/__test__/screens/ErrorHandling.test.tsx new file mode 100644 index 00000000..dcfcf412 --- /dev/null +++ b/frontend/src/__test__/screens/ErrorHandling.test.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { MemoryRouter, Navigate } from 'react-router-dom'; +import { describe, it, expect, vi } from 'vitest'; +import ErrorHandling from '../../screens/ErrorHandling'; +import { useRouteError, isRouteErrorResponse } from 'react-router-dom'; + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom'); + return { + ...actual, + useRouteError: vi.fn(), + isRouteErrorResponse: vi.fn(), + Navigate: vi.fn(({ to, replace }) =>
Navigate to {to} - {replace}
), + }; +}); + +describe('ErrorHandling', () => { + it('should navigate to / when error status is 401', () => { + (useRouteError as vi.Mock).mockReturnValue({ status: 401 }); + (isRouteErrorResponse as vi.Mock).mockReturnValue(true); + + const { getByText } = render( + + + + ); + + expect(getByText('Navigate to / -')).toBeDefined(); + }); + + it('should display Unauthorized when error status is 403', () => { + (useRouteError as vi.Mock).mockReturnValue({ status: 403 }); + (isRouteErrorResponse as vi.Mock).mockReturnValue(true); + + const { getByText } = render( + + + + ); + + expect(getByText('Unauthorized')).toBeDefined(); + }); + + it('should display Page Not Found when error status is 404', () => { + (useRouteError as vi.Mock).mockReturnValue({ status: 404 }); + (isRouteErrorResponse as vi.Mock).mockReturnValue(true); + + const { getByText } = render( + + + + ); + + expect(getByText('Page Not Found')).toBeDefined(); + }); + + it('should display default error message for other types of errors', () => { + (useRouteError as vi.Mock).mockReturnValue({ status: 500 }); + (isRouteErrorResponse as vi.Mock).mockReturnValue(true); + + const { getByText } = render( + + + + ); + + expect(getByText('Oops! Something went wrong')).toBeDefined(); + }); + + it('should display default error message when error is not a route error response', () => { + (useRouteError as vi.Mock).mockReturnValue(new Error('Test error')); + (isRouteErrorResponse as vi.Mock).mockReturnValue(false); + + const { getByText } = render( + + + + ); + + expect(getByText('Oops! Something went wrong')).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/screens/Landing.test.tsx b/frontend/src/__test__/screens/Landing.test.tsx new file mode 100644 index 00000000..964cab22 --- /dev/null +++ b/frontend/src/__test__/screens/Landing.test.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import Landing from '../../screens/Landing'; +import { useGetAuth } from '../../contexts/AuthProvider'; +import { useLottie } from 'lottie-react'; + +vi.mock('../../contexts/AuthProvider', () => ({ + useGetAuth: vi.fn(), +})); + +vi.mock('lottie-react', () => ({ + useLottie: vi.fn(), +})); + +vi.mock('@carbon/react', () => ({ + Button: ({ onClick, children, ...props }) => ( + + ), +})); + +vi.mock('@carbon/icons-react', () => ({ + Login: () => , +})); + +describe('Landing', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('should render the landing page with title and subtitle', () => { + (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: false, login: vi.fn() }); + (useLottie as vi.Mock).mockReturnValue({ View:
Lottie Animation
}); + + const { getByTestId, getByText } = render(); + + expect(getByTestId('landing-title').textContent).toBe('Welcome to SILVA'); + expect(getByTestId('landing-subtitle').textContent).toBe('Plan, report, and analyze your silviculture activities'); + expect(getByText('Lottie Animation')).toBeDefined(); + }); + + it('should call login with "idir" when Login with IDIR button is clicked', () => { + const mockLogin = vi.fn(); + (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: false, login: mockLogin }); + (useLottie as vi.Mock).mockReturnValue({ View:
Lottie Animation
}); + + const { getByTestId } = render(); + + fireEvent.click(getByTestId('landing-button__idir')); + expect(mockLogin).toHaveBeenCalledWith('idir'); + }); + + it('should call login with "bceid" when Login with Business BCeID button is clicked', () => { + const mockLogin = vi.fn(); + (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: false, login: mockLogin }); + (useLottie as vi.Mock).mockReturnValue({ View:
Lottie Animation
}); + + const { getByTestId } = render(); + + fireEvent.click(getByTestId('landing-button__bceid')); + expect(mockLogin).toHaveBeenCalledWith('bceid'); + }); + + it('should redirect to /dashboard if user is already logged in', async () => { + const mockLogin = vi.fn(); + (useGetAuth as vi.Mock).mockReturnValue({ isLoggedIn: true, login: mockLogin }); + (useLottie as vi.Mock).mockReturnValue({ View:
Lottie Animation
}); + + const originalLocation = window.location; + delete window.location; + window.location = { href: '' }; + + const { container } = render(); + + await waitFor(() => { + expect(window.location.href).toContain('/dashboard'); + }); + + // Restore original window.location + window.location = originalLocation; + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/services/AuthService.test.ts b/frontend/src/__test__/services/AuthService.test.ts new file mode 100644 index 00000000..12c8b16c --- /dev/null +++ b/frontend/src/__test__/services/AuthService.test.ts @@ -0,0 +1,130 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { parseToken, getAuthIdToken } from '../../services/AuthService'; +import { formatRolesArray } from '../../utils/famUtils'; +import { JWT } from '../../types/amplify'; + +vi.mock('../../utils/famUtils', () => ({ + formatRolesArray: vi.fn(), +})); + +describe('AuthService', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('parseToken', () => { + it('should return undefined if idToken is undefined', () => { + const result = parseToken(undefined); + expect(result).toBeUndefined(); + }); + + it('should set authIdToken and return parsed user data', () => { + const mockToken: JWT = { + payload: { + "cognito:groups": [ + "group1", + "group2" + ], + "preferred_username": "b5ecdb094dfb4149a6a8445a01a96bf0@idir", + "custom:idp_user_id": "B5ECDB094DFB4149A6A8445A01A96BF0", + "custom:idp_username": "JRYAN", + "custom:idp_display_name": "Ryan, Jack CIA:IN", + "email": "jack.ryan@gov.bc.ca", + "email_verified": false, + "custom:idp_name": "idir", + "given_name": "Jack", + "name": "Jack Ryan", + "family_name": "Ryan", + exp: 1234567890 + }, + toString: () => 'mockTokenString', + }; + + (formatRolesArray as vi.Mock).mockReturnValue([{ role: 'role1' }, { role: 'role2' }]); + + const result = parseToken(mockToken); + + expect(getAuthIdToken()).toBe('mockTokenString'); + expect(result).toEqual({ + userName: 'JRYAN', + displayName: "Ryan, Jack CIA:IN", + email: 'jack.ryan@gov.bc.ca', + idpProvider: 'IDIR', + clientRoles: [{ role: 'role1' }, { role: 'role2' }], + exp: 1234567890, + firstName: 'Jack', + lastName: 'Ryan', + providerUsername: 'IDIR\\JRYAN', + }); + }); + + it('should handle displayName without comma correctly', () => { + const mockToken: JWT = { + payload: { + "cognito:groups": [ + "group1", + "group2" + ], + "preferred_username": "b5ecdb094dfb4149a6a8445a01a96bf0@idir", + "custom:idp_user_id": "B5ECDB094DFB4149A6A8445A01A96BF0", + "custom:idp_username": "JRYAN", + "custom:idp_display_name": "Jack Ryan", + "email": "jack.ryan@gov.bc.ca", + "email_verified": false, + "custom:idp_name": "idir", + "given_name": "Jack", + "name": "Jack Ryan", + "family_name": "Ryan", + exp: 1234567890 + }, + toString: () => 'mockTokenString', + }; + + (formatRolesArray as vi.Mock).mockReturnValue([{ role: 'role1' }, { role: 'role2' }]); + + const result = parseToken(mockToken); + + expect(getAuthIdToken()).toBe('mockTokenString'); + expect(result).toEqual({ + userName: 'JRYAN', + displayName: 'Jack Ryan', + email: 'jack.ryan@gov.bc.ca', + idpProvider: 'IDIR', + clientRoles: [{ role: 'role1' }, { role: 'role2' }], + exp: 1234567890, + firstName: 'Jack', + lastName: 'Ryan', + providerUsername: 'IDIR\\JRYAN', + }); + }); + + it('should handle missing identities correctly', () => { + const mockToken: JWT = { + payload: { + 'custom:idp_display_name': 'Doe, John', + 'custom:idp_username': 'johndoe', + 'email': 'john.doe@example.com', + exp: 1234567890, + }, + toString: () => 'mockTokenString', + }; + + (formatRolesArray as vi.Mock).mockReturnValue([{ role: 'role1' }, { role: 'role2' }]); + + const result = parseToken(mockToken); + + expect(getAuthIdToken()).toBe('mockTokenString'); + expect(result).toEqual({ + userName: 'johndoe', + displayName: 'Doe, John', + email: 'john.doe@example.com', + idpProvider: '', + clientRoles: [{ role: 'role1' }, { role: 'role2' }], + exp: 1234567890, + firstName: 'John', + lastName: 'Doe', + providerUsername: '\\johndoe', + }); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/__test__/utils/famUtils.test.ts b/frontend/src/__test__/utils/famUtils.test.ts new file mode 100644 index 00000000..6dcc807d --- /dev/null +++ b/frontend/src/__test__/utils/famUtils.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from 'vitest'; +import { formatRolesArray, extractGroups } from '../../utils/famUtils'; + +describe('famUtils', () => { + describe('formatRolesArray', () => { + it('should return an empty array if decodedIdToken is undefined', () => { + const result = formatRolesArray(undefined); + expect(result).toEqual([]); + }); + + it('should return an empty array if decodedIdToken does not contain cognito:groups', () => { + const decodedIdToken = {}; + const result = formatRolesArray(decodedIdToken); + expect(result).toEqual([]); + }); + + it('should return formatted roles array if decodedIdToken contains cognito:groups', () => { + const decodedIdToken = { + 'cognito:groups': ['admin_123', 'user_456', 'editor_123'] + }; + const result = formatRolesArray(decodedIdToken); + expect(result).toEqual([ + { + clientId: '123', + roles: ['admin', 'editor'], + clientName: 'Client Number 123' + }, + { + clientId: '456', + roles: ['user'], + clientName: 'Client Number 456' + } + ]); + }); + + it('should handle groups without underscores correctly', () => { + const decodedIdToken = { + 'cognito:groups': ['admin', 'user_456', 'editor_123'] + }; + const result = formatRolesArray(decodedIdToken); + expect(result).toEqual([ + { + clientId: '123', + roles: ['editor'], + clientName: 'Client Number 123' + }, + { + clientId: '456', + roles: ['user'], + clientName: 'Client Number 456' + } + ]); + }); + }); + + describe('extractGroups', () => { + it('should return an empty array if decodedIdToken is undefined', () => { + const result = extractGroups(undefined); + expect(result).toEqual([]); + }); + + it('should return an empty array if decodedIdToken does not contain cognito:groups', () => { + const decodedIdToken = {}; + const result = extractGroups(decodedIdToken); + expect(result).toEqual([]); + }); + + it('should return groups array if decodedIdToken contains cognito:groups', () => { + const decodedIdToken = { + 'cognito:groups': ['admin', 'user', 'editor'] + }; + const result = extractGroups(decodedIdToken); + expect(result).toEqual(['admin', 'user', 'editor']); + }); + }); +}); \ No newline at end of file diff --git a/frontend/src/actions/userAction.ts b/frontend/src/actions/userAction.ts index 03b74b06..87d6369d 100644 --- a/frontend/src/actions/userAction.ts +++ b/frontend/src/actions/userAction.ts @@ -4,9 +4,9 @@ import { USER_DETAILS_FAIL, SET_CLIENT_ROLES } from '../constants/userConstants'; -import { isCurrentAuthUser } from '../services/AuthService'; import { AppDispatch } from '../store'; import { UserClientRolesType } from '../types/UserRoleType'; +import { useGetAuth } from '../contexts/AuthProvider'; const FAM_LOGIN_USER = 'famLoginUser'; @@ -15,15 +15,15 @@ export const getUserDetails = () => async (dispatch: AppDispatch) => { dispatch({ type: USER_DETAILS_REQUEST }); - //first call the isCurrent and only after that extract the JSON - const data = await isCurrentAuthUser(); + //first call the isCurrent and only after that extract the JSON + const { isLoggedIn } = useGetAuth(); const userJSON = localStorage.getItem(FAM_LOGIN_USER); // Retrieve the JSON string from local storage const user = userJSON ? JSON.parse(userJSON) : null; // Parse the JSON string to a JavaScript object dispatch({ type: USER_DETAILS_SUCCESS, - payload: { ...user, isLoggedIn: data } + payload: { ...user, isLoggedIn } }); } catch (error) { dispatch({ diff --git a/frontend/src/amplifyconfiguration.ts b/frontend/src/amplifyconfiguration.ts index 709002c5..31a482f0 100644 --- a/frontend/src/amplifyconfiguration.ts +++ b/frontend/src/amplifyconfiguration.ts @@ -17,8 +17,8 @@ const verificationMethods: verificationMethodsType = 'code'; const amplifyconfig = { Auth: { Cognito: { - userPoolId: env.VITE_USER_POOLS_ID ?? "ca-central-1_t2HSZBHur", - userPoolClientId: env.VITE_USER_POOLS_WEB_CLIENT_ID ?? "70a2am185rie10r78b0ugcs1mm", + userPoolId: env.VITE_USER_POOLS_ID, + userPoolClientId: env.VITE_USER_POOLS_WEB_CLIENT_ID, signUpVerificationMethod: verificationMethods, // 'code' | 'link' loginWith: { oauth: { diff --git a/frontend/src/components/MyProfile/index.tsx b/frontend/src/components/MyProfile/index.tsx index e58ab25d..7cf48f55 100644 --- a/frontend/src/components/MyProfile/index.tsx +++ b/frontend/src/components/MyProfile/index.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; import { SideNavLink } from '@carbon/react'; import { Asleep, Light, UserFollow } from '@carbon/icons-react'; import AvatarImage from '../AvatarImage'; @@ -7,13 +6,12 @@ import { useThemePreference } from '../../utils/ThemePreference'; import PanelSectionName from '../PanelSectionName'; import OrganizationSelection from '../OrganizationSelection'; import './MyProfile.scss'; -import { logout } from '../../services/AuthService'; -import { RootState } from '../../store'; +import { useGetAuth } from '../../contexts/AuthProvider'; const MyProfile = () => { const { theme, setTheme } = useThemePreference(); - const userDetails = useSelector((state: RootState) => state.userDetails) const [goTo, setGoTo] = useState(false); + const { logout, user: authUser } = useGetAuth(); const changeTheme = () => { if (theme === 'g10') { @@ -36,12 +34,12 @@ const MyProfile = () => { <>
- +
-

{`${userDetails.user.firstName} ${userDetails.user.lastName}`}

-

{`IDIR: ${userDetails.user.userName}`}

-

{`Email:${userDetails.user.email}`}

+

{`${authUser?.firstName} ${authUser?.lastName}`}

+

{`IDIR: ${authUser?.userName}`}

+

{`Email:${authUser?.email}`}

diff --git a/frontend/src/components/OrganizationSelection/index.tsx b/frontend/src/components/OrganizationSelection/index.tsx index d0c44062..b33ed7fb 100644 --- a/frontend/src/components/OrganizationSelection/index.tsx +++ b/frontend/src/components/OrganizationSelection/index.tsx @@ -1,236 +1,13 @@ -import React, { useState } from 'react'; -import { - FlexGrid, Row, Column, - ButtonSkeleton, Search, Button, - ContainedListItem -} from '@carbon/react'; -import { ArrowRight } from '@carbon/icons-react'; -import { useQueries, useQueryClient } from '@tanstack/react-query'; -import { logout } from '../../services/AuthService'; -import { setSelectedClientRoles } from '../../actions/selectedClientRolesActions'; -import { getForestClientByNumberOrAcronym } from '../../services/TestService'; -import { THREE_HALF_HOURS, THREE_HOURS } from '../../config/TimeUnits'; -import { UserClientRolesType } from '../../types/UserRoleType'; -import { ForestClientType } from '../../types/ForestClientTypes/ForestClientType'; -import EmptySection from '../EmptySection'; - -import { MIN_CLIENTS_SHOW_SEARCH, TEXT } from './constants'; +import { FlexGrid } from '@carbon/react'; import { RoleSelectionProps } from './definitions'; -import OrganizationItem from './OrganizationItem'; - -const SELECTED_CLIENT_ROLES = 'selectedClientRoles'; - import './styles.scss'; -import { AppDispatch, RootState } from '../../store'; -import { useDispatch, useSelector } from 'react-redux'; - const OrganizationSelection = ({ simpleView }: RoleSelectionProps) => { - const dispatch = useDispatch(); - const userDetails = useSelector((state: RootState) => state.userDetails); - - const user = userDetails.user; - const selectedClientRoles = useSelector((state: RootState) => state.selectedClientRoles); - - const [matchedClients, setMatchedClients] = useState([]); - const [searchTerm, setSearchTerm] = useState(''); - const [clientRolesToSet, setClientRolesToSet] = useState(selectedClientRoles); - - useQueries({ - queries: user?.clientRoles?.map((clientRole:UserClientRolesType) => ({ - queryKey: ['role', 'forest-clients', clientRole.clientId], - queryFn: () => getForestClientByNumberOrAcronym(clientRole.clientId), - staleTime: THREE_HOURS, - cacheTime: THREE_HALF_HOURS, - refetchOnReconnect: false - })) ?? [] - }); - - const qc = useQueryClient(); - - const filterClientsByValue = (value: string) => { - const forestClientsQueriesData = qc.getQueriesData({ queryKey: ['role', 'forest-clients'] }); - - const forestClients = forestClientsQueriesData.map((qData) => ( - qData.at(1) as ForestClientType - )); - - const loweredSearchTerm = value.toLowerCase(); - - const foundByName = forestClients - .filter((fc) => (fc.clientName.toLowerCase().includes(loweredSearchTerm))); - - const foundById = forestClients - .filter((fc) => (fc.clientNumber.includes(loweredSearchTerm))); - - const foundCombined = foundByName.concat(foundById); - - const foundIds = foundCombined.map((fc) => fc.clientNumber); - - setSearchTerm(value); - setMatchedClients(foundIds); - }; - - const setSelectedClientRolesHandler = (clientId: string, clientName?: string) => { - if (clientId) { - const found = user?.clientRoles?.find((uClientRole: UserClientRolesType) => ( - uClientRole.clientId === clientId - )); - if (found) { - const toSet: UserClientRolesType = { - ...found, - clientName - }; - setClientRolesToSet(toSet); - if (simpleView) { - localStorage.setItem(SELECTED_CLIENT_ROLES, JSON.stringify(toSet)) - dispatch(setSelectedClientRoles(toSet)); - } - } - } - }; - - const continueToDashboard = () => { - if (clientRolesToSet) { - localStorage.setItem(SELECTED_CLIENT_ROLES, JSON.stringify(clientRolesToSet)) - dispatch(setSelectedClientRoles(clientRolesToSet)); - } - }; - - const renderOrgItem = (clientRole: UserClientRolesType) => { - const queryKey = ['role', 'forest-clients', clientRole.clientId]; - const queryState = qc.getQueryState(queryKey); - const queryData: ForestClientType | undefined = qc.getQueryData(queryKey); - - //then it would automatically be loading, but the loading is not working still need to determine - if (queryState?.status !== 'success' && queryState?.status !== 'error') { - return ( - - - - - - ); - } - - if ( - ( - matchedClients.length === 0 - && searchTerm === '' - ) - || matchedClients.includes(clientRole.clientId) - ) { - return ( - setSelectedClientRolesHandler(clientRole.clientId, queryData?.clientName)} - > - - - ); - } - - return null; - }; - - const renderListSection = () => { - if (user?.clientRoles.length === 0) { - return ( - - - - ); - } - - if (searchTerm.length > 0 && matchedClients.length === 0) { - return ( - - - {TEXT.emptySearch} -

- )} - /> -
- ); - } - - return ( - -
    - { - user?.clientRoles - .map((clientRole:any) => ( - renderOrgItem(clientRole) - )) - } -
-
- ); - }; + return ( - - { - user!.clientRoles.length > MIN_CLIENTS_SHOW_SEARCH - ? ( - - - ) => filterClientsByValue(e.target.value) - } - /> - - - ) : null - } - - { - renderListSection() - } - - {simpleView?null:( - - - - - - - - - )} + ); }; -export default OrganizationSelection; +export default OrganizationSelection; \ No newline at end of file diff --git a/frontend/src/contexts/AuthProvider.tsx b/frontend/src/contexts/AuthProvider.tsx new file mode 100644 index 00000000..72517541 --- /dev/null +++ b/frontend/src/contexts/AuthProvider.tsx @@ -0,0 +1,133 @@ +import React, { createContext, useState, useContext, useEffect, useMemo, ReactNode } from 'react'; +import { fetchAuthSession, signInWithRedirect, signOut } from "aws-amplify/auth"; +import { parseToken, FamLoginUser } from "../services/AuthService"; +import { extractGroups } from '../utils/famUtils'; +import { env } from '../env'; +import { JWT } from '../types/amplify'; + +// 1. Define an interface for the context value +interface AuthContextType { + user: FamLoginUser | undefined; + userRoles: string[] | undefined; + isLoggedIn: boolean; + isLoading: boolean; + login: (provider: string) => void; + logout: () => void; + +} + +// 2. Define an interface for the provider's props +interface AuthProviderProps { + children: ReactNode; +} + +// 3. Create the context with a default value of `undefined` +const AuthContext = createContext(undefined); + +// 4. Create the AuthProvider component with explicit typing +export const AuthProvider: React.FC = ({ children }) => { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [user, setUser] = useState(undefined); + const [userRoles, setUserRoles] = useState(undefined); + const [isLoading, setIsLoading] = useState(true); + + const appEnv = env.VITE_ZONE ?? 'DEV'; + + + useEffect(() => { + const checkUser = async () => { + try{ + const idToken = await loadUserToken(); + setIsLoggedIn(!!idToken); + setIsLoading(false); + if(idToken){ + setUser(parseToken(idToken)); + setUserRoles(extractGroups(idToken?.payload)); + } + }catch(error){ + setIsLoggedIn(false); + setUser(parseToken(undefined)); + setIsLoading(false); + } + }; + checkUser(); + }, []); + + const login = async (provider: string) => { + const envProvider = (provider.localeCompare('idir') === 0) + ?`${(appEnv).toLocaleUpperCase()}-IDIR` + : `${(appEnv).toLocaleUpperCase()}-BCEIDBUSINESS`; + + signInWithRedirect({ + provider: { custom: envProvider.toUpperCase() } + }); + }; + + const logout = async () => { + await signOut(); + setIsLoggedIn(false); + window.location.href = '/'; // Optional redirect after logout + }; + + const contextValue = useMemo(() => ({ + user, + userRoles, + isLoggedIn, + isLoading, + login, + logout + }), [user, userRoles, isLoggedIn, isLoading]); + + return ( + + {children} + + ); +}; + +// This is a helper hook to use the Auth context more easily +// 5. Create a custom hook to consume the context safely +export const useGetAuth = (): AuthContextType => { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useGetAuth must be used within an AuthProvider'); + } + return context; +}; + +const loadUserToken = async () : Promise => { + if(env.NODE_ENV !== 'test'){ + const {idToken} = (await fetchAuthSession()).tokens ?? {}; + return Promise.resolve(idToken); + } else { + // This is for test only + const token = getUserTokenFromCookie(); + if (token) { + const jwtBody = token + ? JSON.parse(atob(token.split(".")[1])) + : null; + return Promise.resolve({ payload: jwtBody }); + } else { + return Promise.reject(new Error("No token found")); + } + } +}; + +const getUserTokenFromCookie = (): string|undefined => { + const baseCookieName = `CognitoIdentityServiceProvider.${env.VITE_USER_POOLS_WEB_CLIENT_ID}`; + const userId = encodeURIComponent(getCookie(`${baseCookieName}.LastAuthUser`)); + if (userId) { + const idTokenCookieName = `${baseCookieName}.${userId}.idToken`; + const idToken = getCookie(idTokenCookieName); + return idToken; + } else { + return undefined; + } +}; + +const getCookie = (name: string): string => { + const cookie = document.cookie + .split(";") + .find((cookieValue) => cookieValue.trim().startsWith(name)); + return cookie ? cookie.split("=")[1] : ""; +}; \ No newline at end of file diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 5d1bf9bf..48e9d8d6 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,6 +1,5 @@ window.global ||= window; import React from 'react'; -import ReactDOM from 'react-dom'; import './index.css'; import { ClassPrefix } from '@carbon/react'; import { Provider } from 'react-redux' @@ -12,6 +11,11 @@ import PaginationProvider from './contexts/PaginationProvider'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; import { OpeningsSearchProvider } from './contexts/search/OpeningsSearch'; +import { Amplify } from 'aws-amplify'; +import amplifyconfig from './amplifyconfiguration'; +import { CookieStorage } from 'aws-amplify/utils'; +import { cognitoUserPoolsTokenProvider } from 'aws-amplify/auth/cognito'; +import { AuthProvider } from './contexts/AuthProvider'; const container: HTMLElement | null = document.getElementById('root'); if (container) { @@ -41,19 +45,25 @@ if (container) { } }); + Amplify.configure(amplifyconfig); + cognitoUserPoolsTokenProvider.setKeyValueStorage(new CookieStorage()); + + root.render( - - - - - - - - - + + + + + + + + + + + diff --git a/frontend/src/routes/PostLoginRoute.tsx b/frontend/src/routes/PostLoginRoute.tsx deleted file mode 100644 index dc784805..00000000 --- a/frontend/src/routes/PostLoginRoute.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { useEffect } from 'react'; -import { Navigate } from 'react-router-dom'; -import { getUserDetails } from '../actions/userAction'; -import { Loading } from "@carbon/react"; -import { RootState } from '../store'; -import { useDispatch, useSelector } from 'react-redux'; - -interface IProps { - children: JSX.Element; -} - -const PostLoginRoute = ({ children }: IProps): JSX.Element => { - const userDetails = useSelector((state: RootState) => state.userDetails) - const { loading, error, user } = userDetails - const { pathname } = window.location; - const encodedUrl = encodeURI(`/?page=${pathname}`); - - const dispatch:any = useDispatch(); - useEffect(()=>{ - dispatch(getUserDetails()) - },[dispatch]) - - return ( - loading ? ( - - ): - error? ( -
Error
- ):( - (() => { - if(user?.isLoggedIn === true){ - return children; - } - else if(user?.isLoggedIn === false){ - return ; - } - - return <> - })() - ) - ); -}; - -export default PostLoginRoute ; \ No newline at end of file diff --git a/frontend/src/routes/ProtectedRoute.tsx b/frontend/src/routes/ProtectedRoute.tsx index 82f7b7b8..d719c0e4 100644 --- a/frontend/src/routes/ProtectedRoute.tsx +++ b/frontend/src/routes/ProtectedRoute.tsx @@ -1,33 +1,37 @@ import React from 'react'; -import { Navigate } from 'react-router-dom'; -import { RootState } from '../store'; -import { useSelector } from 'react-redux'; +import { Navigate, Outlet } from 'react-router-dom'; +import { useGetAuth } from '../contexts/AuthProvider'; +import { Loading } from "@carbon/react"; -interface IProps { - children: JSX.Element; +interface ProtectedRouteProps { + requireAuth?: boolean; + requiredRoles?: string[]; + redirectTo?: string; } -const ProtectedRoute = ({ children }: IProps): JSX.Element => { - const userDetails = useSelector((state: RootState) => state.userDetails); - const { error, user } = userDetails; - const { pathname } = window.location; - const encodedUrl = encodeURI(`/?page=${pathname}`); - return ( - error? ( -
Error
- ):( - (() => { - if(user?.isLoggedIn){ - return children; - } - else if(user?.isLoggedIn === false){ - return ; - } +const ProtectedRoute: React.FC = ({ + requireAuth = false, + requiredRoles = [], + redirectTo = '/' +}) => { + const { isLoggedIn, isLoading, userRoles } = useGetAuth(); - return <>Sorry ; - })() - ) - ); + if(isLoading) { + return ; + } + + // 1. If authentication is required and the user is not logged in, redirect to login + if (requireAuth && !isLoggedIn) { + return ; + } + + // 2. If specific roles are required and user does not have them, redirect to unauthorized page + if (requiredRoles.length > 0 && !requiredRoles.some(role => userRoles?.includes(role))) { + return ; + } + + // 3. If all checks pass, render child routes + return ; }; export default ProtectedRoute; diff --git a/frontend/src/screens/Dashboard/Dashboard.scss b/frontend/src/screens/Dashboard/Dashboard.scss deleted file mode 100644 index e5709c3c..00000000 --- a/frontend/src/screens/Dashboard/Dashboard.scss +++ /dev/null @@ -1,4 +0,0 @@ -.landing-grid{ - padding-right: 0; - height: 100vh; - } \ No newline at end of file diff --git a/frontend/src/screens/Dashboard/index.tsx b/frontend/src/screens/Dashboard/index.tsx deleted file mode 100644 index f7cee077..00000000 --- a/frontend/src/screens/Dashboard/index.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import { useSelector } from "react-redux"; -import { RootState } from "../../store"; - -const Dashboard: React.FC = () => { - const userDetails = useSelector((state: RootState) => state.userDetails); - const selectedClientRoles = useSelector((state: RootState) => state.selectedClientRoles); - const { user } = userDetails; - - return ( - <> -
-

- Hello  - {user.firstName+" "+user.lastName}, - welcome to the SILVA portal. You are logged in with the client  - {selectedClientRoles?.clientName}!!

-
- - ); -}; - -export default Dashboard; diff --git a/frontend/src/screens/DashboardRedirect/index.tsx b/frontend/src/screens/DashboardRedirect/index.tsx index a2f0ffb5..c8e32c9f 100644 --- a/frontend/src/screens/DashboardRedirect/index.tsx +++ b/frontend/src/screens/DashboardRedirect/index.tsx @@ -1,32 +1,13 @@ import React from "react"; -import { useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import LoginOrgSelection from "../../views/LoginOrgSelection"; -import SideLayout from "../../layouts/SideLayout"; -import Opening from "../Opening"; -import { RootState } from "../../store"; -import { useSelector } from "react-redux"; +import { Navigate } from "react-router-dom"; +import { Loading } from "@carbon/react"; +import { useGetAuth } from "../../contexts/AuthProvider"; const DashboardRedirect: React.FC = () => { - const userDetails = useSelector((state: RootState) => state.userDetails); - const { user } = userDetails; - - const navigate = useNavigate(); - - // Redirect logic based on selectedClientRoles existence - useEffect(() => { - if (user) { - navigate("/opening"); - } - }, [user]); - + const { isLoggedIn } = useGetAuth(); return ( <> - {user ? ( - } /> - ) : ( - - )} + {isLoggedIn ? : } ); }; diff --git a/frontend/src/screens/ErrorHandling/index.tsx b/frontend/src/screens/ErrorHandling/index.tsx new file mode 100644 index 00000000..5b9d74e1 --- /dev/null +++ b/frontend/src/screens/ErrorHandling/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { isRouteErrorResponse, useRouteError, Navigate } from 'react-router-dom'; + +const ErrorHandling: React.FC = () => { + const error = useRouteError(); + + console.log('ErrorHandling',error); + + if (isRouteErrorResponse(error)) { + if (error.status === 401) { + return + } else if (error.status === 403) { + return

Unauthorized

; + } else if (error.status === 404) { + return

Page Not Found

; + } + } + + // Default error message for other types of errors + return

Oops! Something went wrong

; +}; + +export default ErrorHandling; diff --git a/frontend/src/screens/Landing/index.tsx b/frontend/src/screens/Landing/index.tsx index df22534e..22d0668f 100644 --- a/frontend/src/screens/Landing/index.tsx +++ b/frontend/src/screens/Landing/index.tsx @@ -1,35 +1,29 @@ -import React, { useCallback } from "react"; +import React from "react"; import BCGovLogo from "../../components/BCGovLogo"; -import { Button, InlineNotification } from "@carbon/react"; +import { Button } from "@carbon/react"; import { Login } from '@carbon/icons-react'; -import { signIn } from "../../services/AuthService"; import './Landing.scss'; import '../../custom.scss'; import { useLottie } from "lottie-react"; import silvaLottie from "../../assets/lotties/silva-logo-lottie-1.json" -import ThemeToggle from "../../components/ThemeToggle"; -import { useNavigate } from "react-router-dom"; +import { useGetAuth } from "../../contexts/AuthProvider"; const Landing: React.FC = () => { + + const { login, isLoggedIn } = useGetAuth(); + // Adding the Lottie Loader and loading the View for lottie with initial options const options = { animationData: silvaLottie, loop: true }; const { View } = useLottie(options); - const navigate = useNavigate(); - const login = useCallback(async (provider: string) => { - try { - await signIn(provider) - } catch(e) { - if (e && typeof e === "object" && "message" in e) { - const messageError = e.message as string; - if (messageError === 'There is already a signed in user.') { - navigate('/dashboard'); - } - } - } - }, []); + + // If the user is already logged in, redirect to the dashboard + if (isLoggedIn) { + window.location.href = '/dashboard'; + } + return ( <>
@@ -61,7 +55,7 @@ const Landing: React.FC = () => {
+ + + +
+ ); +}; + +describe('OpeningsSearchProvider', () => { + beforeEach(() => { + render( + + + + ); + }); + + it('should initialize with default values', () => { + expect(screen.getByTestId('searchTerm').textContent).toBe(''); + expect(screen.getByTestId('startDate').textContent).toBe('null'); + }); + + it('should update searchTerm', () => { + fireEvent.click(screen.getByTestId('setSearchTerm')); + expect(screen.getByTestId('searchTerm').textContent).toBe('test search'); + }); + + it('should set and then clear filters', () => { + fireEvent.click(screen.getByTestId('setFilters')); + expect(screen.getByTestId('startDate').textContent).not.toBe('null'); + + fireEvent.click(screen.getByTestId('clearFilters')); + expect(screen.getByTestId('startDate').textContent).toBe('null'); + }); + + it('should clear individual field', () => { + fireEvent.click(screen.getByTestId('setFilters')); + expect(screen.getByTestId('startDate').textContent).not.toBe('null'); + + fireEvent.click(screen.getByTestId('clearStartDate')); + expect(screen.getByTestId('startDate').textContent).toBe('null'); + }); +}); diff --git a/frontend/src/__test__/services/search/openings.test.tsx b/frontend/src/__test__/services/search/openings.test.tsx new file mode 100644 index 00000000..cbed5b1f --- /dev/null +++ b/frontend/src/__test__/services/search/openings.test.tsx @@ -0,0 +1,109 @@ +// fetchOpenings.test.ts +import axios from "axios"; +import "@testing-library/jest-dom"; +import { fetchOpenings, OpeningFilters } from "../../../services/search/openings"; +import { getAuthIdToken } from "../../../services/AuthService"; +import { createDateParams } from "../../../utils/searchUtils"; +import { describe, it, beforeEach, afterEach, vi, expect } from "vitest"; + +// Mock dependencies +vi.mock("axios"); +vi.mock("../../../services/AuthService"); +vi.mock("../../../utils/searchUtils"); + +// Define mocked functions and modules +const mockedAxios = axios as vi.Mocked; +const mockedGetAuthIdToken = getAuthIdToken as vi.Mock; +const mockedCreateDateParams = createDateParams as vi.Mock; + +// Sample filters +const sampleFilters: OpeningFilters = { + searchInput: "", + startDate: "2024-11-19", + endDate: "2024-11-21", + orgUnit: ["DCC", "DCK", "DCR"], + category: ["EXCLU", "CONT"], + status: ["DFT", "APP"], + clientAcronym: "12", + blockStatus: "", + cutBlock: "L", + cuttingPermit: "PC", + timberMark: "123", + dateType: "Disturbance", + openingFilters: ["Openings created by me", "Submitted to FRPA section 108"], + blockStatuses: [], + page: 1, + perPage: 5, +}; + +// Mock response from the backend API +const mockApiResponse = { + data: { + pageIndex: 0, + perPage: 5, + totalPages: 100, + hasNextPage: false, + data: [ + { + openingId: 9100129, + openingNumber: "98", + cuttingPermitId: "S", + timberMark: "W1729S", + cutBlockId: "06-03", + orgUnitCode: "DPG", + orgUnitName: "Prince George Natural Resource District", + entryUserId: "Datafix107808", + statusCode: "APP", + statusDescription: "Approved", + categoryCode: "FTML", + categoryDescription: "Forest Tenure - Major Licensee", + }, + ], + }, +}; + +describe("fetchOpenings", () => { + beforeEach(() => { + mockedGetAuthIdToken.mockReturnValue("mocked-token"); + mockedCreateDateParams.mockReturnValue({ + dateStartKey: "disturbanceStartDate", + dateEndKey: "disturbanceEndDate", + }); + mockedAxios.get.mockResolvedValue(mockApiResponse); // Mock API response + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should fetch openings with the correct parameters and return flattened data", async () => { + const result = await fetchOpenings(sampleFilters); + + // Verify that axios was called with the correct URL and headers + expect(mockedAxios.get).toHaveBeenCalledWith( + expect.stringContaining("/api/opening-search?"), + expect.objectContaining({ + headers: { + Authorization: `Bearer mocked-token`, + }, + }) + ); + + // Check if the result data matches the expected flattened structure + expect(result.data[0].openingId).toEqual(9100129); + }); + + it("should handle an empty response data array gracefully", async () => { + mockedAxios.get.mockResolvedValueOnce({ data: { data: [] } }); + const result = await fetchOpenings(sampleFilters); + + // Ensure the function returns an empty array when the response is empty + expect(result.data).toEqual([]); + }); + + it("should throw an error when the API request fails", async () => { + mockedAxios.get.mockRejectedValueOnce(new Error("Network error")); + + await expect(fetchOpenings(sampleFilters)).rejects.toThrow("Network error"); + }); +}); diff --git a/frontend/src/services/search/openings.ts b/frontend/src/services/search/openings.ts index 55ff3d18..c8d33643 100644 --- a/frontend/src/services/search/openings.ts +++ b/frontend/src/services/search/openings.ts @@ -9,10 +9,11 @@ export interface OpeningFilters { searchInput?: string; startDate?: string; endDate?: string; - orgUnit?: string; - category?: string; + orgUnit?: string[]; + category?: string[]; clientAcronym?: string; blockStatus?: string; + dateType?: string; cutBlock?: string; cuttingPermit?: string; grossArea?: string; From d144d040801f4e042caf7da3f63f5543f157e873 Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 6 Nov 2024 15:05:04 -0800 Subject: [PATCH 11/12] added more test for the Openings Search Bar --- .../Openings/OpeningSearchBar.test.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx index 3f99fe56..ea3fabce 100644 --- a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx @@ -53,4 +53,48 @@ describe("OpeningsSearchBar", () => { // Check if the onSearchClick function was called expect(onSearchClick).toHaveBeenCalled(); }); + + it("should show AdvancedSearchDropdown if isOpen is true", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + const isOpen = false; + // Mock the useState calls + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [true, vi.fn()]) // Mocking isOpen state as false + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false + .mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state + .mockImplementationOnce(() => [0, vi.fn()]) // Mocking filtersCount state + .mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state + render( + + + + ); + + // Check if an element with the class 'd-none' exists within the structure + const dNoneElement = screen.getAllByText("", {selector: ".d-block"})[0]; + expect(dNoneElement).toBeInTheDocument(); + }); + + it("should not show AdvancedSearchDropdown if isOpen is false", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + const isOpen = false; + // Mock the useState calls + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking isOpen state as false + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false + .mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state + .mockImplementationOnce(() => [0, vi.fn()]) // Mocking filtersCount state + .mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state + render( + + + + ); + + // Check if an element with the class 'd-none' exists within the structure + const dNoneElement = screen.getAllByText("", {selector: ".d-none"})[0]; + expect(dNoneElement).toBeInTheDocument(); + }); }); \ No newline at end of file From 66d6ae80de9338af606a4c03b31dda39c7cb9dec Mon Sep 17 00:00:00 2001 From: jazzgrewal Date: Wed, 6 Nov 2024 15:25:07 -0800 Subject: [PATCH 12/12] added more test for the OpeningsSearchBar --- .../Openings/OpeningSearchBar.test.tsx | 26 ++++++++++++++++++- .../services/search/openings.test.tsx | 4 +-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx index ea3fabce..8044d8d6 100644 --- a/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx +++ b/frontend/src/__test__/components/SilvicultureSearch/Openings/OpeningSearchBar.test.tsx @@ -6,7 +6,7 @@ import "@testing-library/jest-dom"; import OpeningsSearchBar from "../../../../components/SilvicultureSearch/Openings/OpeningsSearchBar"; import { vi } from "vitest"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; +import { OpeningsSearchProvider, useOpeningsSearch } from "../../../../contexts/search/OpeningsSearch"; // Mock the useOpeningsSearch context to avoid rendering errors vi.mock("../../../../contexts/search/OpeningsSearch", () => ({ @@ -97,4 +97,28 @@ describe("OpeningsSearchBar", () => { const dNoneElement = screen.getAllByText("", {selector: ".d-none"})[0]; expect(dNoneElement).toBeInTheDocument(); }); + + it("should show correct filter count, when count is greater than 0", () => { + // Create a mock function to pass as a prop + const onSearchClick = vi.fn(); + // Mock the useState calls + vi.spyOn(React, 'useState') + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking isOpen state as false + .mockImplementationOnce(() => [false, vi.fn()]) // Mocking showFilters state as false + .mockImplementationOnce(() => ["", vi.fn()]) // Mocking searchInput state + .mockImplementationOnce(() => [2, vi.fn()]) // Mocking filtersCount state + .mockImplementationOnce(() => [null, vi.fn()]); // Mocking filtersList state + + render( + + + + ); + + console.log(screen.debug()); + + // Check if an element with the class 'd-none' exists within the structure + const dNoneElement = screen.getByText('+2'); + expect(dNoneElement).toBeInTheDocument(); + }); }); \ No newline at end of file diff --git a/frontend/src/__test__/services/search/openings.test.tsx b/frontend/src/__test__/services/search/openings.test.tsx index cbed5b1f..6ddbf33e 100644 --- a/frontend/src/__test__/services/search/openings.test.tsx +++ b/frontend/src/__test__/services/search/openings.test.tsx @@ -78,13 +78,13 @@ describe("fetchOpenings", () => { it("should fetch openings with the correct parameters and return flattened data", async () => { const result = await fetchOpenings(sampleFilters); - + const expectedToken = 'mocked-token'; // Verify that axios was called with the correct URL and headers expect(mockedAxios.get).toHaveBeenCalledWith( expect.stringContaining("/api/opening-search?"), expect.objectContaining({ headers: { - Authorization: `Bearer mocked-token`, + Authorization: `Bearer ${expectedToken}`, }, }) );