From f50ddab0262046d1932f9feaa4c56f8abaf59932 Mon Sep 17 00:00:00 2001 From: ateklu7 <129882603+ateklu7@users.noreply.github.com> Date: Fri, 17 Nov 2023 13:38:15 -0500 Subject: [PATCH 1/4] Jira ent update (#11523) Co-authored-by: araya Co-authored-by: araya Co-authored-by: Brax Excell Co-authored-by: BearHanded --- .github/workflows/deploy.yml | 4 +-- .../scan_security-hub-jira-integration.yml | 16 +++++----- .../workflows/scan_snyk-jira-integration.yml | 19 ++++++------ .github/workflows/snyk-auto-merge.yml | 29 +++++++++++++++++++ 4 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/snyk-auto-merge.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 127b97429..de4486b6a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -60,9 +60,9 @@ jobs: env: SLS_DEPRECATION_DISABLE: "*" # Turn off deprecation warnings in the pipeline steps: - - name: set branch_name + - name: set branch_name # Some integrations (Dependabot & Snyk) build very long branch names. This is a switch to make long branch names shorter. run: | - if [[ "$GITHUB_REF" =~ ^refs/heads/dependabot/.* ]]; then # Dependabot builds very long branch names. This is a switch to make it shorter. + if [[ "$GITHUB_REF" =~ ^refs/heads/dependabot/.* ]] || [[ "$GITHUB_REF" =~ ^refs/remotes/origin/snyk-upgrade-* ]] || [[ "$GITHUB_REF" =~ ^refs/remotes/origin/snyk-fix-* ]]; then echo "branch_name=`echo ${GITHUB_REF#refs/heads/} | md5sum | head -c 10 | sed 's/^/x/'`" >> $GITHUB_ENV else echo "branch_name=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV diff --git a/.github/workflows/scan_security-hub-jira-integration.yml b/.github/workflows/scan_security-hub-jira-integration.yml index 14699a9fb..1de1e2cd5 100644 --- a/.github/workflows/scan_security-hub-jira-integration.yml +++ b/.github/workflows/scan_security-hub-jira-integration.yml @@ -1,4 +1,4 @@ -name: Sync Security Hub findings and Jira issues +name: Scan and Open Jira Tickets (AWS Security Hub) on: workflow_dispatch: # for testing and manual runs @@ -20,16 +20,14 @@ jobs: with: aws-region: ${{ secrets.AWS_DEFAULT_REGION }} role-to-assume: ${{ secrets.PRODUCTION_SYNC_OIDC_ROLE }} - - name: Sync Security Hub and Jira uses: Enterprise-CMCS/mac-fc-security-hub-visibility@v1.0.5 with: - jira-token: ${{ secrets.JIRA_SERVICE_USER_TOKEN }} - jira-username: ${{ secrets.JIRA_SERVICE_USERNAME }} - jira-host: qmacbis.atlassian.net - jira-project-key: MDCT - jira-epic-key: MDCT-2280 + jira-username: "mdct_github_service_account" + jira-token: ${{ secrets.JIRA_ENT_USER_TOKEN }} + jira-host: jiraent.cms.gov + jira-project-key: CMDCT jira-ignore-statuses: Done, Closed, Canceled - jira-custom-fields: '{ "customfield_14154" : [{"id": "16958", "value": "MCR"}] }' + jira-custom-fields: '{ "customfield_10100": "CMDCT-2280", "customfield_26700" : [{"id": "40104", "value": "MCR"}] }' aws-severities: CRITICAL, HIGH, MEDIUM - assign-jira-ticket-to: ${{ secrets.ACCOUNT_ID_REHMAN }} + assign-jira-ticket-to: "MWTW" diff --git a/.github/workflows/scan_snyk-jira-integration.yml b/.github/workflows/scan_snyk-jira-integration.yml index 72900c08f..77203fa1c 100644 --- a/.github/workflows/scan_snyk-jira-integration.yml +++ b/.github/workflows/scan_snyk-jira-integration.yml @@ -1,4 +1,4 @@ -name: Snyk Scan and Report +name: Scan and Open Jira Tickets (Snyk) on: pull_request: @@ -14,7 +14,6 @@ jobs: name: Snyk Run (for PR and push) runs-on: ubuntu-latest if: github.event_name == 'pull_request' - steps: - name: Check out repository uses: actions/checkout@v3 @@ -42,17 +41,17 @@ jobs: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - name: use the custom github action to parse Snyk output - uses: Enterprise-CMCS/macfc-security-scan-report@v2.7.0 + uses: Enterprise-CMCS/macfc-security-scan-report@v2.7.4 with: - jira-username: ${{ secrets.JIRA_SERVICE_USERNAME }} - jira-token: ${{ secrets.JIRA_SERVICE_USER_TOKEN }} - jira-host: "qmacbis.atlassian.net" - jira-project-key: "MDCT" + jira-username: "mdct_github_service_account" + jira-token: ${{ secrets.JIRA_ENT_USER_TOKEN }} + jira-host: "jiraent.cms.gov" + jira-project-key: "CMDCT" jira-issue-type: "Task" - jira-custom-field-key-value: '{ "customfield_10007" : "MDCT-2280", "customfield_14154" : [{"id": "16958", "value": "MCR"}] }' + jira-custom-field-key-value: '{ "customfield_10100": "CMDCT-2280", "customfield_26700" : [{"id": "40104", "value": "MCR"}] }' jira-labels: "MCR,snyk" jira-title-prefix: "[MCR] - Snyk :" - is_jira_enterprise: false - assign-jira-ticket-to: ${{ secrets.ACCOUNT_ID_REHMAN }} + is_jira_enterprise: true + assign-jira-ticket-to: "MWTW" scan-output-path: "snyk_output.txt" scan-type: "snyk" diff --git a/.github/workflows/snyk-auto-merge.yml b/.github/workflows/snyk-auto-merge.yml new file mode 100644 index 000000000..ece635257 --- /dev/null +++ b/.github/workflows/snyk-auto-merge.yml @@ -0,0 +1,29 @@ +# Adapted from https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions +name: Snyk auto-merge +on: + pull_request: + workflow_dispatch: + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'mdct-github-service-account' }} + steps: + - name: Snyk Gather Metadata + id: metadata + uses: dependabot/fetch-metadata@v1 + - name: Approve a PR + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + - name: Enable auto-merge for Snyk PRs + if: ${{ steps.metadata.outputs.update-type != 'version-update:semver-major'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} From fcc73dfbbd7ddf061f99ccf1b54332ebaf551834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karla=20Valc=C3=A1rcel=20Mart=C3=ADnez?= <99458559+karla-vm@users.noreply.github.com> Date: Tue, 21 Nov 2023 08:56:28 -0500 Subject: [PATCH 2/4] =?UTF-8?q?Admin=20Banner=20Zustandification=E2=84=A2?= =?UTF-8?q?=EF=B8=8F=20=20(Karla's=20Version)=20(#11525)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/ui-src/package.json | 3 +- .../banners/AdminBannerProvider.test.tsx | 10 +- .../banners/AdminBannerProvider.tsx | 100 +++++++++++++----- .../components/pages/Admin/AdminPage.test.tsx | 74 +++++++------ .../src/components/pages/Admin/AdminPage.tsx | 61 ++++------- .../src/components/pages/Home/HomePage.tsx | 12 +-- services/ui-src/src/types/banners.ts | 6 -- services/ui-src/src/types/index.ts | 1 + services/ui-src/src/types/states.ts | 18 ++++ services/ui-src/src/utils/index.ts | 2 + services/ui-src/src/utils/state/useStore.ts | 49 +++++++++ .../ui-src/src/utils/testing/mockZustand.tsx | 19 ++++ .../ui-src/src/utils/testing/setupJest.tsx | 2 + services/ui-src/src/verbiage/errors.ts | 1 + services/ui-src/yarn.lock | 12 +++ 15 files changed, 256 insertions(+), 114 deletions(-) create mode 100644 services/ui-src/src/types/states.ts create mode 100644 services/ui-src/src/utils/state/useStore.ts create mode 100644 services/ui-src/src/utils/testing/mockZustand.tsx diff --git a/services/ui-src/package.json b/services/ui-src/package.json index 74b81fd3f..85682b70e 100644 --- a/services/ui-src/package.json +++ b/services/ui-src/package.json @@ -41,7 +41,8 @@ "react-scripts": "^5.0.0", "react-uuid": "^1.0.3", "sass": "^1.37.5", - "yup": "^0.32.11" + "yup": "^0.32.11", + "zustand": "^4.4.6" }, "devDependencies": { "@aws-sdk/types": "^3.38.0", diff --git a/services/ui-src/src/components/banners/AdminBannerProvider.test.tsx b/services/ui-src/src/components/banners/AdminBannerProvider.test.tsx index e40d3a9a7..00585f26e 100644 --- a/services/ui-src/src/components/banners/AdminBannerProvider.test.tsx +++ b/services/ui-src/src/components/banners/AdminBannerProvider.test.tsx @@ -5,6 +5,7 @@ import { act } from "react-dom/test-utils"; // components import { AdminBannerContext, AdminBannerProvider } from "./AdminBannerProvider"; // utils +import { useStore } from "utils"; import { mockBannerData } from "utils/testing/setupJest"; import { bannerErrors } from "verbiage/errors"; @@ -27,7 +28,6 @@ const TestComponent = () => { - {context.errorMessage &&

{context.errorMessage}

} ); }; @@ -70,7 +70,9 @@ describe("Test AdminBannerProvider fetchAdminBanner method", () => { await act(async () => { await render(testComponent); }); - expect(screen.queryByText(bannerErrors.GET_BANNER_FAILED)).toBeVisible(); + expect(useStore.getState().bannerErrorMessage).toBe( + bannerErrors.GET_BANNER_FAILED + ); }); }); @@ -89,7 +91,9 @@ describe("Test AdminBannerProvider deleteAdminBanner method", () => { }); expect(mockAPI.deleteBanner).toHaveBeenCalledTimes(1); expect(mockAPI.deleteBanner).toHaveBeenCalledWith(mockBannerData.key); - await waitFor(() => expect(mockAPI.getBanner).toHaveBeenCalledTimes(1)); + + // 1 call on render + 1 call on button click + await waitFor(() => expect(mockAPI.getBanner).toHaveBeenCalledTimes(2)); }); }); diff --git a/services/ui-src/src/components/banners/AdminBannerProvider.tsx b/services/ui-src/src/components/banners/AdminBannerProvider.tsx index 2426ae0f4..b3c1faf6d 100644 --- a/services/ui-src/src/components/banners/AdminBannerProvider.tsx +++ b/services/ui-src/src/components/banners/AdminBannerProvider.tsx @@ -1,66 +1,118 @@ -import { useState, createContext, ReactNode, useMemo, useEffect } from "react"; +import { createContext, ReactNode, useMemo, useEffect } from "react"; // utils -import { AdminBannerData, AdminBannerShape } from "types/banners"; +import { AdminBannerData, AdminBannerMethods } from "types/banners"; import { bannerId } from "../../constants"; import { bannerErrors } from "verbiage/errors"; // api -import { deleteBanner, getBanner, writeBanner } from "utils"; +import { + deleteBanner, + getBanner, + writeBanner, + useStore, + checkDateRangeStatus, +} from "utils"; const ADMIN_BANNER_ID = bannerId; -export const AdminBannerContext = createContext({ - bannerData: undefined as AdminBannerData | undefined, +export const AdminBannerContext = createContext({ fetchAdminBanner: Function, writeAdminBanner: Function, deleteAdminBanner: Function, - isLoading: false as boolean, - errorMessage: undefined, }); export const AdminBannerProvider = ({ children }: Props) => { - const [bannerData, setBannerData] = useState( - undefined - ); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(); + // state management + const { + bannerData, + setBannerData, + bannerActive, + setBannerActive, + bannerLoading, + setBannerLoading, + bannerErrorMessage, + setBannerErrorMessage, + bannerDeleting, + setBannerDeleting, + } = useStore(); const fetchAdminBanner = async () => { - setIsLoading(true); + setBannerLoading(true); try { const currentBanner = await getBanner(ADMIN_BANNER_ID); const newBannerData = currentBanner?.Item || {}; setBannerData(newBannerData); - } catch (e: any) { - setIsLoading(false); - setError(bannerErrors.GET_BANNER_FAILED); + setBannerErrorMessage(""); + } catch (error: any) { + setBannerLoading(false); + setBannerErrorMessage(bannerErrors.GET_BANNER_FAILED); } - setIsLoading(false); + setBannerLoading(false); }; const deleteAdminBanner = async () => { - await deleteBanner(ADMIN_BANNER_ID); - setBannerData(undefined); + setBannerDeleting(true); + try { + await deleteBanner(ADMIN_BANNER_ID); + await fetchAdminBanner(); + } catch (error: any) { + setBannerErrorMessage(bannerErrors.DELETE_BANNER_FAILED); + } + setBannerDeleting(false); }; const writeAdminBanner = async (newBannerData: AdminBannerData) => { - await writeBanner(newBannerData); - setBannerData(newBannerData); + try { + await writeBanner(newBannerData); + } catch (error: any) { + setBannerErrorMessage(bannerErrors.CREATE_BANNER_FAILED); + } + await fetchAdminBanner(); }; + useEffect(() => { + let bannerActivity = false; + if (bannerData) { + bannerActivity = checkDateRangeStatus( + bannerData.startDate, + bannerData.endDate + ); + } + setBannerActive(bannerActivity); + }, [bannerData]); + useEffect(() => { fetchAdminBanner(); }, []); const providerValue = useMemo( () => ({ + // banner data bannerData, + setBannerData, + // banner is showing + bannerActive, + setBannerActive, + // banner is loading + bannerLoading, + setBannerLoading, + // banner error state + bannerErrorMessage, + setBannerErrorMessage, + // banner deleting state + bannerDeleting, + setBannerDeleting, + // banner API calls fetchAdminBanner, writeAdminBanner, deleteAdminBanner, - isLoading: isLoading, - errorMessage: error, }), - [bannerData, isLoading, error] + [ + bannerData, + bannerActive, + bannerLoading, + bannerErrorMessage, + bannerDeleting, + ] ); return ( diff --git a/services/ui-src/src/components/pages/Admin/AdminPage.test.tsx b/services/ui-src/src/components/pages/Admin/AdminPage.test.tsx index 21469548b..bea51bc6d 100644 --- a/services/ui-src/src/components/pages/Admin/AdminPage.test.tsx +++ b/services/ui-src/src/components/pages/Admin/AdminPage.test.tsx @@ -8,7 +8,10 @@ import { AdminPage, AdminBannerContext } from "components"; import { RouterWrappedComponent, mockBannerData, + mockBannerStore, } from "utils/testing/setupJest"; +import { useStore } from "utils"; +import { bannerErrors } from "verbiage/errors"; const mockBannerMethods = { fetchAdminBanner: jest.fn(() => {}), @@ -16,19 +19,8 @@ const mockBannerMethods = { deleteAdminBanner: jest.fn(() => {}), }; -const mockContextWithoutBanner = { - ...mockBannerMethods, - bannerData: undefined, - isLoading: false, - errorData: null, -}; - -const mockContextWithBanner = { - ...mockBannerMethods, - bannerData: mockBannerData, - isLoading: false, - errorData: null, -}; +jest.mock("utils/state/useStore"); +const mockedUseStore = useStore as jest.MockedFunction; const adminView = (context: any) => ( @@ -41,12 +33,13 @@ const adminView = (context: any) => ( describe("Test AdminPage banner manipulation functionality", () => { it("Deletes current banner on delete button click", async () => { await act(async () => { - await render(adminView(mockContextWithBanner)); + mockedUseStore.mockReturnValue(mockBannerStore); + await render(adminView(mockBannerMethods)); }); const deleteButton = screen.getByText("Delete Current Banner"); await userEvent.click(deleteButton); await waitFor(() => - expect(mockContextWithBanner.deleteAdminBanner).toHaveBeenCalled() + expect(mockBannerMethods.deleteAdminBanner).toHaveBeenCalled() ); }); }); @@ -54,7 +47,11 @@ describe("Test AdminPage banner manipulation functionality", () => { describe("Test AdminPage without banner", () => { beforeEach(async () => { await act(async () => { - await render(adminView(mockContextWithoutBanner)); + mockedUseStore.mockReturnValue({ + ...mockBannerStore, + bannerData: undefined, + }); + await render(adminView(mockBannerMethods)); }); }); @@ -75,7 +72,8 @@ describe("Test AdminPage without banner", () => { describe("Test AdminPage with banner", () => { beforeEach(async () => { await act(async () => { - await render(adminView(mockContextWithBanner)); + mockedUseStore.mockReturnValue(mockBannerStore); + await render(adminView(mockBannerMethods)); }); }); @@ -101,12 +99,21 @@ describe("Test AdminPage with banner", () => { describe("Test AdminPage with active/inactive banner", () => { const currentTime = Date.now(); // 'current' time in ms since unix epoch const oneDay = 1000 * 60 * 60 * 24; // 1000ms * 60s * 60m * 24h = 86,400,000ms - const context = mockContextWithBanner; + const context = mockBannerMethods; + mockedUseStore.mockReturnValue(mockBannerStore); test("Active banner shows 'active' status", async () => { - context.bannerData.startDate = currentTime - oneDay; - context.bannerData.endDate = currentTime + oneDay; + const activeBannerData = { + ...mockBannerData, + startDate: currentTime - oneDay, + endDate: currentTime + oneDay, + }; await act(async () => { + mockedUseStore.mockReturnValue({ + ...mockBannerStore, + bannerData: activeBannerData, + bannerActive: true, + }); await render(adminView(context)); }); const currentBannerStatus = screen.getByText("Status:"); @@ -114,9 +121,16 @@ describe("Test AdminPage with active/inactive banner", () => { }); test("Inactive banner shows 'inactive' status", async () => { - context.bannerData.startDate = currentTime + oneDay; - context.bannerData.endDate = currentTime + oneDay + oneDay; + const inactiveBannerData = { + ...mockBannerData, + startDate: currentTime + oneDay, + endDate: currentTime + oneDay + oneDay, + }; await act(async () => { + mockedUseStore.mockReturnValue({ + ...mockBannerStore, + bannerData: inactiveBannerData, + }); await render(adminView(context)); }); const currentBannerStatus = screen.getByText("Status:"); @@ -126,14 +140,14 @@ describe("Test AdminPage with active/inactive banner", () => { describe("Test AdminPage delete banner error handling", () => { it("Displays error if deleteBanner throws error", async () => { - window.HTMLElement.prototype.scrollIntoView = jest.fn(); - const context = mockContextWithBanner; - context.deleteAdminBanner = jest.fn(() => { - throw new Error(); - }); await act(async () => { - await render(adminView(context)); + mockedUseStore.mockReturnValue({ + ...mockBannerStore, + bannerErrorMessage: bannerErrors.DELETE_BANNER_FAILED, + }); + await render(adminView(mockBannerMethods)); }); + const deleteButton = screen.getByText("Delete Current Banner"); await userEvent.click(deleteButton); expect(screen.getByText("Error")).toBeVisible(); @@ -142,14 +156,14 @@ describe("Test AdminPage delete banner error handling", () => { describe("Test AdminPage accessibility", () => { it("Should not have basic accessibility issues without banner", async () => { - const { container } = render(adminView(mockContextWithoutBanner)); + const { container } = render(adminView(mockBannerMethods)); await act(async () => { expect(await axe(container)).toHaveNoViolations(); }); }); it("Should not have basic accessibility issues with banner", async () => { - const { container } = render(adminView(mockContextWithBanner)); + const { container } = render(adminView(mockBannerMethods)); await act(async () => { expect(await axe(container)).toHaveNoViolations(); }); diff --git a/services/ui-src/src/components/pages/Admin/AdminPage.tsx b/services/ui-src/src/components/pages/Admin/AdminPage.tsx index fabfe4ea6..29db5825e 100644 --- a/services/ui-src/src/components/pages/Admin/AdminPage.tsx +++ b/services/ui-src/src/components/pages/Admin/AdminPage.tsx @@ -1,4 +1,4 @@ -import { useState, useContext, useEffect } from "react"; +import { useContext, MouseEventHandler } from "react"; // components import { Box, @@ -17,50 +17,25 @@ import { PageTemplate, } from "components"; // utils -import { checkDateRangeStatus, convertDateUtcToEt } from "utils"; -import { bannerErrors } from "verbiage/errors"; +import { convertDateUtcToEt, useStore } from "utils"; import verbiage from "verbiage/pages/admin"; export const AdminPage = () => { + const { deleteAdminBanner, writeAdminBanner } = + useContext(AdminBannerContext); + + // state management const { bannerData, - deleteAdminBanner, - writeAdminBanner, - isLoading, - errorMessage, - } = useContext(AdminBannerContext); - const [error, setError] = useState(errorMessage); - const [deleting, setDeleting] = useState(false); - const [isBannerActive, setIsBannerActive] = useState(false); - - useEffect(() => { - let bannerActivity = false; - if (bannerData) { - bannerActivity = checkDateRangeStatus( - bannerData.startDate, - bannerData.endDate - ); - } - setIsBannerActive(bannerActivity); - }, [bannerData]); - - useEffect(() => { - setError(errorMessage); - }, [errorMessage]); - - const deleteBanner = async () => { - setDeleting(true); - try { - await deleteAdminBanner(); - } catch (error: any) { - setError(bannerErrors.DELETE_BANNER_FAILED); - } - setDeleting(false); - }; + bannerActive, + bannerLoading, + bannerErrorMessage, + bannerDeleting, + } = useStore(); return ( - + {verbiage.intro.header} @@ -69,7 +44,7 @@ export const AdminPage = () => { Current Banner - {isLoading ? ( + {bannerLoading ? ( @@ -81,8 +56,8 @@ export const AdminPage = () => { Status:{" "} - - {isBannerActive ? "Active" : "Inactive"} + + {bannerActive ? "Active" : "Inactive"} @@ -99,9 +74,9 @@ export const AdminPage = () => {