From e82d0db215fc2c5095953ce61a56fb38f490f0a7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 12:46:13 -0400 Subject: [PATCH 1/5] Deployment of 2024-05-03 (#14290) * Adding API * Adding auth stuff * Adding attempts * Fixing client issue * Added auth header for bearer token and populating response into the proper object * Adding auth piece * I think this works * Initial stab at changing the history API authorization rules to allow receiver organizations * Do some clean-up and extraction * Make the list of receiving organizations a set * Split up the authorization tests in SubmissionsFacadeTests * checking other organizations unit test passes * Fixing auth changes * Remove receiving org from the actions in the submissions facade tests because they would not be filled in in real life * Comment out additional auth tests for now * Fixing query so that it no longer errors and returns both sender and receiver name * Linter * Revert database access code so it works for our usecase again * Reverting authTest comments, and auth changes * re-adding linebreak to remove all changes from the file * Adding env vars to TF * Adding env vars and build updates * Adding retrieval of action id * Fixing cast and response code * Adding changes for submission function * Move some logic around * Adding tests for submisison function and dependency for co-routine testing * Adding working test * Refactoring api endpoints to comply with Azure demands * Shuffling tests * refactor retrieveMetadata to not be so duplicative * reduce duplicate lines? * giving better name to endpoints * Update prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt refactor function name to be more specific Co-authored-by: Arnej <118766341+arnejduranovic@users.noreply.github.com> * Calling newly refactored parent function * Adding endpoint documentation and comments for endpoints * Adding documentation * Update function reference in tests * start refactoring * Fixing UUID refactor * Fixing tests * Fixing other test * Adding test changes * Rename for consistency * Adding test coverage * Use an external transaction and customize what descendant task action to search for * update comment to match update code * check http status before returning * add some more tests * Adding comments to explain functions * Fixing api docs * Adding doc updates * Use the elvis operator * Transition to using getRootReport (singular) * Update prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt Co-authored-by: Michael Kalish * Update prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt Co-authored-by: Michael Kalish * Use async await instead of launch * Use the ReportService in the DeliveryFunction instead of ReportGraph directly * Remove ETOR_TI_baseurl env var from local run * Check for the ETOR_TI_baseurl environment variable, and return a 500 if missing * Bump the applicationinsights group in /frontend-react with 2 updates (#14214) Bumps the applicationinsights group in /frontend-react with 2 updates: [@microsoft/applicationinsights-react-js](https://github.com/microsoft/applicationinsights-react-js) and [@microsoft/applicationinsights-web](https://github.com/microsoft/ApplicationInsights-JS). Updates `@microsoft/applicationinsights-react-js` from 17.1.1 to 17.1.2 - [Release notes](https://github.com/microsoft/applicationinsights-react-js/releases) - [Changelog](https://github.com/microsoft/applicationinsights-react-js/blob/main/RELEASES.md) - [Commits](https://github.com/microsoft/applicationinsights-react-js/compare/17.1.1...17.1.2) Updates `@microsoft/applicationinsights-web` from 3.1.1 to 3.2.0 - [Release notes](https://github.com/microsoft/ApplicationInsights-JS/releases) - [Changelog](https://github.com/microsoft/ApplicationInsights-JS/blob/main/RELEASES.md) - [Commits](https://github.com/microsoft/ApplicationInsights-JS/commits) --- updated-dependencies: - dependency-name: "@microsoft/applicationinsights-react-js" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: applicationinsights - dependency-name: "@microsoft/applicationinsights-web" dependency-type: direct:production update-type: version-update:semver-minor dependency-group: applicationinsights ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump vite from 5.2.8 to 5.2.10 in /frontend-react in the vite group (#14133) Bumps the vite group in /frontend-react with 1 update: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite). Updates `vite` from 5.2.8 to 5.2.10 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.2.10/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: direct:development update-type: version-update:semver-patch dependency-group: vite ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump the vitest group across 1 directory with 4 updates (#14254) * Bump the vitest group across 1 directory with 4 updates Bumps the vitest group with 4 updates in the /frontend-react directory: [@vitest/coverage-istanbul](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-istanbul), [@vitest/ui](https://github.com/vitest-dev/vitest/tree/HEAD/packages/ui), [eslint-plugin-vitest](https://github.com/veritem/eslint-plugin-vitest) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest). Updates `@vitest/coverage-istanbul` from 1.4.0 to 1.5.3 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v1.5.3/packages/coverage-istanbul) Updates `@vitest/ui` from 1.4.0 to 1.5.3 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v1.5.3/packages/ui) Updates `eslint-plugin-vitest` from 0.4.1 to 0.5.4 - [Release notes](https://github.com/veritem/eslint-plugin-vitest/releases) - [Commits](https://github.com/veritem/eslint-plugin-vitest/compare/v0.4.1...v0.5.4) Updates `vitest` from 1.4.0 to 1.5.3 - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Commits](https://github.com/vitest-dev/vitest/commits/v1.5.3/packages/vitest) --- updated-dependencies: - dependency-name: "@vitest/coverage-istanbul" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: vitest - dependency-name: "@vitest/ui" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: vitest - dependency-name: eslint-plugin-vitest dependency-type: direct:development update-type: version-update:semver-minor dependency-group: vitest - dependency-name: vitest dependency-type: direct:development update-type: version-update:semver-minor dependency-group: vitest ... Signed-off-by: dependabot[bot] * Use legacy eslint vitest ruleset --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joseph Andersen <12385932+jpandersen87@users.noreply.github.com> * restore sonarcloud frontend only scan config (#14268) * Pump the TI ETOR base URL values into the function application * Temporarily made alert_version_upgrade.yml to trigger on pushes to `dkrylov/metabase-checker-13962` * Temporarily set channel to `p-cdc-prime` * Output `env.Schedules_UpgradeDetails_0_UpgradeRequired` for testing. * Corrected the condition for Slack notification: `${{ env.Schedules_UpgradeDetails_0_UpgradeRequired == 'true'}}` * A test. * Undo test. * Undo more test. * Undo more test. * Undo more test. * Bump bridgecrewio/checkov-action from 12.2726.0 to 12.2731.0 (#14275) Bumps [bridgecrewio/checkov-action](https://github.com/bridgecrewio/checkov-action) from 12.2726.0 to 12.2731.0. - [Release notes](https://github.com/bridgecrewio/checkov-action/releases) - [Commits](https://github.com/bridgecrewio/checkov-action/compare/e5aec874aca04a0b30142382f0980e5145252333...80c58e67aea1b38e55b0097c4efe915e6b04f11d) --- updated-dependencies: - dependency-name: bridgecrewio/checkov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Experience/14179/e2e daily data updates (#14246) * 12974 - e2e updates to daily data * 12974 - more tests * 12974 - more e2e tests * 12974 - more e2e tests * 12974 - more e2e tests * 12974 - more e2e tests * 12974 - fixed failures * 12974 - added no receiver selected tests. * 12974 - added admin filter tests. * 12974 - fixed failing tests * 12974 - added call to getTableRows method where applicable. * 12974 - Added search fileName/reportId tests. Updated methods so that they remain "pure". * 12974 - Changed to use mock data * Added mock when fetching by reportId * update * Removed VITE_IDLE_TIMEOUT=25000 from cli command * Cleanup commented code Moved expect. Added check for filename. * skipped daily-data-page.spec.ts * Added wait to filter-status to get populated. * update * higher retries * increase retries * delay mock resolves * Revert "delay mock resolves" This reverts commit 5053fef3b350d227e1e6d6ad4c0357ffd4656108. * Commented code that needs to be revisited once using live data * Skipped test that need to be revisited * fixing e2e --------- Co-authored-by: Joseph Andersen <12385932+jpandersen87@users.noreply.github.com> * Bug: eagerly intialize FhirPathUtils in the convert step (#14286) * Bug: eagerly intialize FhirPathUtils in the convert step * fixup! Bug: eagerly intialize FhirPathUtils in the convert step --------- Signed-off-by: dependabot[bot] Co-authored-by: Jeff Crichlake Co-authored-by: halprin Co-authored-by: jcrichlake <145698165+jcrichlake@users.noreply.github.com> Co-authored-by: Sylvie Co-authored-by: Arnej <118766341+arnejduranovic@users.noreply.github.com> Co-authored-by: Michael Kalish Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joseph Andersen <12385932+jpandersen87@users.noreply.github.com> Co-authored-by: Stephen Nesman <94193373+snesm@users.noreply.github.com> Co-authored-by: Denis Krylov Co-authored-by: Penelope Lischer <102491809+penny-lischer@users.noreply.github.com> --- .github/actions/sonarcloud/action.yml | 1 + .github/workflows/alert_version_upgrade.yml | 4 +- .github/workflows/validate_terraform.yml | 2 +- frontend-react/.eslintrc.cjs | 2 +- .../e2e/spec/daily-data-page.spec.ts | 312 +++++++------ frontend-react/package.json | 14 +- frontend-react/yarn.lock | 411 ++++++++--------- .../terraform/modules/function_app/locals.tf | 1 + .../terraform/modules/function_app/~inputs.tf | 1 + operations/app/terraform/vars/demo/locals.tf | 1 + operations/app/terraform/vars/demo/main.tf | 1 + operations/app/terraform/vars/prod/locals.tf | 1 + operations/app/terraform/vars/prod/main.tf | 1 + .../app/terraform/vars/staging/locals.tf | 1 + operations/app/terraform/vars/staging/main.tf | 1 + operations/app/terraform/vars/test/locals.tf | 1 + operations/app/terraform/vars/test/main.tf | 1 + prime-router/build.gradle.kts | 1 + prime-router/docs/api/delivery.yml | 27 +- prime-router/docs/api/submissions.yml | 28 +- .../docs/design/design/rs-ti-integration.md | 15 + .../getting-started/getting-started.md | 5 + .../kotlin/fhirengine/engine/FHIRConverter.kt | 12 + .../kotlin/history/azure/DeliveryFunction.kt | 41 ++ .../history/azure/ReportFileFunction.kt | 99 +++- .../history/azure/SubmissionFunction.kt | 43 ++ .../src/main/kotlin/history/db/ReportGraph.kt | 49 +- .../history/azure/DeliveryFunctionTests.kt | 209 ++++++++- .../history/azure/SubmissionFunctionTests.kt | 421 +++++++++++++++++- .../test/kotlin/history/db/ReportGraphTest.kt | 34 +- 30 files changed, 1347 insertions(+), 393 deletions(-) create mode 100644 prime-router/docs/design/design/rs-ti-integration.md diff --git a/.github/actions/sonarcloud/action.yml b/.github/actions/sonarcloud/action.yml index 3c0c75bf5e4..67838d177b1 100644 --- a/.github/actions/sonarcloud/action.yml +++ b/.github/actions/sonarcloud/action.yml @@ -46,6 +46,7 @@ runs: -Dsonar.organization=cdcgov -Dsonar.java.libraries=prime-router/build/libs/*.jar,prime-router/build/**/*.jar -Dsonar.coverage.jacoco.xmlReportPaths=prime-router/build/reports/jacoco/test/jacocoTestReport.xml + -Dsonar.exclusions=prime-router/src/main/java/** - name: Run Backend SonarCloud Scan if: inputs.scan-level == 'backend' diff --git a/.github/workflows/alert_version_upgrade.yml b/.github/workflows/alert_version_upgrade.yml index 4d5df9d58cc..b0db27f3f2a 100644 --- a/.github/workflows/alert_version_upgrade.yml +++ b/.github/workflows/alert_version_upgrade.yml @@ -3,7 +3,7 @@ name: Alert if upgrade is necessary on: workflow_dispatch: schedule: - - cron: "7 13 * * Mon" + - cron: "7 13 * * Mon" jobs: alert_version_upgrade: @@ -35,7 +35,7 @@ jobs: prefix: Schedules - name: Slack Notification - if: ${{ env.Schedules_UpgradeDetails_0_UpgradeRequired }} == 'true' && ${{ env.Schedules_UpgradeDetails_0_UpgradeRequired }} !='null' + if: ${{ env.Schedules_UpgradeDetails_0_UpgradeRequired == 'true'}} uses: ./.github/actions/notifications with: method: slack diff --git a/.github/workflows/validate_terraform.yml b/.github/workflows/validate_terraform.yml index 4639454f19b..d52fba48410 100644 --- a/.github/workflows/validate_terraform.yml +++ b/.github/workflows/validate_terraform.yml @@ -48,7 +48,7 @@ jobs: uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b - name: Run Checkov action - uses: bridgecrewio/checkov-action@e5aec874aca04a0b30142382f0980e5145252333 + uses: bridgecrewio/checkov-action@80c58e67aea1b38e55b0097c4efe915e6b04f11d with: directory: operations/app/terraform skip_check: CKV_AZURE_139,CKV_AZURE_137,CKV_AZURE_103,CKV_AZURE_104,CKV_AZURE_102,CKV_AZURE_130,CKV_AZURE_121,CKV_AZURE_159,CKV_AZURE_67,CKV_AZURE_56,CKV_AZURE_78,CKV_AZURE_17,CKV_AZURE_63,CKV_AZURE_18,CKV_AZURE_88,CKV_AZURE_65,CKV_AZURE_13,CKV_AZURE_66,CKV_AZURE_33,CKV_AZURE_80,CKV_AZURE_35,CKV_AZURE_36,CKV_AZURE_98,CKV_AZURE_1,CKV_AZURE_15,CKV2_AZURE_1,CKV2_AZURE_8,CKV2_AZURE_15,CKV2_AZURE_21,CKV2_AZURE_18,CKV_SECRET_6,CKV_AZURE_190,CKV_AZURE_213,CKV_AZURE_59,CKV2_AZURE_33,CKV2_AZURE_32,CKV2_AZURE_28,CKV_AZURE_206,CKV_AZURE_42,CKV_AZURE_110,CKV_AZURE_109,CKV_AZURE_166,CKV2_AZURE_38,CKV2_AZURE_40,CKV2_AZURE_41,CKV_AZURE_235,CKV2_AZURE_47 diff --git a/frontend-react/.eslintrc.cjs b/frontend-react/.eslintrc.cjs index 83a0d280403..45385778282 100644 --- a/frontend-react/.eslintrc.cjs +++ b/frontend-react/.eslintrc.cjs @@ -59,7 +59,7 @@ module.exports = { ], extends: [ "plugin:testing-library/react", - "plugin:vitest/recommended", + "plugin:vitest/legacy-recommended", "plugin:jest-dom/recommended", ], rules: { diff --git a/frontend-react/e2e/spec/daily-data-page.spec.ts b/frontend-react/e2e/spec/daily-data-page.spec.ts index 8794e1aac74..55d0082ac5c 100644 --- a/frontend-react/e2e/spec/daily-data-page.spec.ts +++ b/frontend-react/e2e/spec/daily-data-page.spec.ts @@ -41,7 +41,7 @@ import { const defaultStartTime = "9:00am"; const defaultEndTime = "11:30pm"; -test.describe.skip("Daily Data page", () => { +test.describe("Daily Data page", () => { test.describe("not authenticated", () => { test("redirects to login", async ({ page }) => { await dailyData.goto(page); @@ -174,7 +174,7 @@ test.describe.skip("Daily Data page", () => { await filterReset(page).click(); }); - test("table loads with selected receiver data", async ({ + test.skip("table loads with selected receiver data", async ({ page, }) => { await page @@ -187,7 +187,7 @@ test.describe.skip("Daily Data page", () => { await expectTableColumnValues( page, 5, - `ignore.${TEST_ORG_IGNORE_RECEIVER}`, + `${TEST_ORG_IGNORE_RECEIVER}`, ); // Check filter status lists receiver value @@ -310,7 +310,9 @@ test.describe.skip("Daily Data page", () => { await expect(applyButton(page)).toBeEnabled(); }); - test("with 'From' date and 'To' date", async ({ page }) => { + test.skip("with 'From' date and 'To' date", async ({ + page, + }) => { const fromDate = await setDate(page, "#start-date", 14); const toDate = await setDate(page, "#end-date", 0); @@ -350,15 +352,16 @@ test.describe.skip("Daily Data page", () => { defaultStartTime, ); + // TODO: uncomment code to use with live data // Check filter status lists receiver value - const filterStatusText = filterStatus(page, [ - TEST_ORG_IGNORE_RECEIVER, - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${defaultStartTime}–${"11:59pm"}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // const filterStatusText = filterStatus(page, [ + // TEST_ORG_IGNORE_RECEIVER, + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // `${defaultStartTime}–${"11:59pm"}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); }); test("with 'From' date, 'To' date, 'End time'", async ({ @@ -381,18 +384,19 @@ test.describe.skip("Daily Data page", () => { defaultEndTime, ); + // TODO: uncomment code to use with live data // Check filter status lists receiver value - const filterStatusText = filterStatus(page, [ - TEST_ORG_IGNORE_RECEIVER, - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${"12:00am"}–${defaultEndTime}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // const filterStatusText = filterStatus(page, [ + // TEST_ORG_IGNORE_RECEIVER, + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // `${"12:00am"}–${defaultEndTime}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); }); - test("with 'From' date, 'To' date, 'Start time', 'End time'", async ({ + test.skip("with 'From' date, 'To' date, 'Start time', 'End time'", async ({ page, }) => { const fromDate = await setDate(page, "#start-date", 14); @@ -406,7 +410,7 @@ test.describe.skip("Daily Data page", () => { .locator(".usa-table tbody") .waitFor({ state: "visible" }); - // Only needed when using live data + // TODO: uncomment code to use with live data // Check that table data contains the dates/times that were selected // const areDatesInRange = // await tableColumnDateTimeInRange( @@ -562,7 +566,9 @@ test.describe.skip("Daily Data page", () => { await expect(applyButton(page)).toBeDisabled(); }); - test("with 'From' date and 'To' date", async ({ page }) => { + test.skip("with 'From' date and 'To' date", async ({ + page, + }) => { const fromDate = await setDate(page, "#start-date", 14); const toDate = await setDate(page, "#end-date", 0); @@ -601,14 +607,15 @@ test.describe.skip("Daily Data page", () => { defaultStartTime, ); + // TODO: uncomment code to use with live data // Check filter status lists receiver value - const filterStatusText = filterStatus(page, [ - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${defaultStartTime}–${"11:59pm"}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // const filterStatusText = filterStatus(page, [ + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // `${defaultStartTime}–${"11:59pm"}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); }); test("with 'From' date, 'To' date, 'End time'", async ({ @@ -631,17 +638,18 @@ test.describe.skip("Daily Data page", () => { defaultEndTime, ); + // TODO: uncomment code to use with live data // Check filter status lists receiver value - const filterStatusText = filterStatus(page, [ - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${"12:00am"}–${defaultEndTime}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // const filterStatusText = filterStatus(page, [ + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // `${"12:00am"}–${defaultEndTime}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); }); - test("with 'From' date, 'To' date, 'Start time', 'End time'", async ({ + test.skip("with 'From' date, 'To' date, 'Start time', 'End time'", async ({ page, }) => { const fromDate = await setDate(page, "#start-date", 14); @@ -655,7 +663,7 @@ test.describe.skip("Daily Data page", () => { .locator(".usa-table tbody") .waitFor({ state: "visible" }); - // Only needed when using live data + // TODO: uncomment code to use with live data // Check that table data contains the dates/times that were selected // const areDatesInRange = // await tableColumnDateTimeInRange( @@ -710,21 +718,24 @@ test.describe.skip("Daily Data page", () => { await searchInput(page).fill(reportId); await searchButton(page).click(); - const rowCount = await tableRows(page).count(); - expect(rowCount).toEqual(1); + // TODO: uncomment code to use with live data + // const rowCount = await tableRows(page).count(); + // expect(rowCount).toEqual(1); // Check filter status lists receiver value - let filterStatusText = filterStatus(page, [reportId]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // let filterStatusText = filterStatus(page, [reportId]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); // Perform search with filters selected await page .locator("#receiver-dropdown") .selectOption(TEST_ORG_IGNORE_RECEIVER); - const fromDate = await setDate(page, "#start-date", 14); - const toDate = await setDate(page, "#end-date", 0); + // const fromDate = await setDate(page, "#start-date", 14); + // const toDate = await setDate(page, "#end-date", 0); + await setDate(page, "#start-date", 14); + await setDate(page, "#end-date", 0); await applyButton(page).click(); await page @@ -732,13 +743,13 @@ test.describe.skip("Daily Data page", () => { .waitFor({ state: "visible" }); // Check filter status lists receiver value - filterStatusText = filterStatus(page, [ - TEST_ORG_IGNORE_RECEIVER, - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // filterStatusText = filterStatus(page, [ + // TEST_ORG_IGNORE_RECEIVER, + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); // Check search is cleared await expect(searchInput(page)).toHaveValue(""); @@ -746,7 +757,7 @@ test.describe.skip("Daily Data page", () => { }); test.describe("search", () => { - test("returns match for Report ID", async ({ page }) => { + test.skip("returns match for Report ID", async ({ page }) => { const reportId = await tableDataCellValue(page, 0, 0); await searchInput(page).fill(reportId); await searchButton(page).click(); @@ -766,7 +777,7 @@ test.describe.skip("Daily Data page", () => { ); }); - test("returns match for Filename", async ({ page }) => { + test.skip("returns match for Filename", async ({ page }) => { const fileName = await tableDataCellValue(page, 2, 4); await searchInput(page).fill(fileName); await searchButton(page).click(); @@ -796,12 +807,15 @@ test.describe.skip("Daily Data page", () => { }); test("clears filters on search", async ({ page }) => { + // TODO: uncomment code to use with live data // Perform search with all filters selected await page .locator("#receiver-dropdown") .selectOption(TEST_ORG_IGNORE_RECEIVER); - const fromDate = await setDate(page, "#start-date", 14); - const toDate = await setDate(page, "#end-date", 0); + // const fromDate = await setDate(page, "#start-date", 14); + // const toDate = await setDate(page, "#end-date", 0); + await setDate(page, "#start-date", 14); + await setDate(page, "#end-date", 0); await setTime(page, "#start-time", defaultStartTime); await setTime(page, "#end-time", defaultEndTime); @@ -811,32 +825,29 @@ test.describe.skip("Daily Data page", () => { .waitFor({ state: "visible" }); // Check filter status lists receiver value - let filterStatusText = filterStatus(page, [ - TEST_ORG_IGNORE_RECEIVER, - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${defaultStartTime}–${defaultEndTime}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // let filterStatusText = filterStatus(page, [ + // TEST_ORG_IGNORE_RECEIVER, + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // `${defaultStartTime}–${defaultEndTime}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); const reportId = "729158ce-4125-46fa-bea0-3c0f910f472c"; await searchInput(page).fill(reportId); await searchButton(page).click(); - const rowCount = await tableRows(page).count(); - expect(rowCount).toEqual(1); - // Check filter status lists receiver value - filterStatusText = filterStatus(page, [reportId]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // filterStatusText = filterStatus(page, [reportId]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); //Check table data matches search - expect(await tableDataCellValue(page, 0, 0)).toEqual( - reportId, - ); + // expect(await tableDataCellValue(page, 0, 0)).toEqual( + // reportId, + // ); // Check filters are cleared await expect(receiverDropdown(page)).toHaveValue(""); @@ -853,9 +864,7 @@ test.describe.skip("Daily Data page", () => { }); test("has pagination", async ({ page }) => { - await expect( - page.getByTestId("Deliveries pagination"), - ).toBeAttached(); + await expect(page.getByTestId("Pagination")).toBeAttached(); }); }); }); @@ -968,7 +977,7 @@ test.describe.skip("Daily Data page", () => { await expectTableColumnValues( page, 5, - `ak-phd.${TEST_ORG_AK_RECEIVER}`, + `${TEST_ORG_AK_RECEIVER}`, ); // Check filter status lists receiver value @@ -1061,7 +1070,9 @@ test.describe.skip("Daily Data page", () => { await expect(applyButton(page)).toBeEnabled(); }); - test("with 'From' date and 'To' date", async ({ page }) => { + test.skip("with 'From' date and 'To' date", async ({ + page, + }) => { const fromDate = await setDate(page, "#start-date", 14); const toDate = await setDate(page, "#end-date", 0); @@ -1102,14 +1113,14 @@ test.describe.skip("Daily Data page", () => { ); // Check filter status lists receiver value - const filterStatusText = filterStatus(page, [ - TEST_ORG_AK_RECEIVER, - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${defaultStartTime}–${"11:59pm"}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // const filterStatusText = filterStatus(page, [ + // TEST_ORG_AK_RECEIVER, + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // `${defaultStartTime}–${"11:59pm"}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); }); test("with 'From' date, 'To' date, 'End time'", async ({ @@ -1133,17 +1144,17 @@ test.describe.skip("Daily Data page", () => { ); // Check filter status lists receiver value - const filterStatusText = filterStatus(page, [ - TEST_ORG_AK_RECEIVER, - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${"12:00am"}–${defaultEndTime}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // const filterStatusText = filterStatus(page, [ + // TEST_ORG_AK_RECEIVER, + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // `${"12:00am"}–${defaultEndTime}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); }); - test("with 'From' date, 'To' date, 'Start time', 'End time'", async ({ + test.skip("with 'From' date, 'To' date, 'Start time', 'End time'", async ({ page, }) => { const fromDate = await setDate(page, "#start-date", 14); @@ -1285,16 +1296,20 @@ test.describe.skip("Daily Data page", () => { // Apply button is enabled await applyButton(page).click(); await page - .locator(".usa-table tbody") - .waitFor({ state: "visible" }); + .getByTestId("filter-status") + .waitFor({ timeout: 3000 }); + + // Form values persist + await expect(startDate(page)).toHaveValue(fromDate); + await expect(endDate(page)).toHaveValue(toDate); // Check filter status lists receiver value - const filterStatusText = filterStatus(page, [ - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // const filterStatusText = filterStatus(page, [ + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); }); test("with 'From' date, 'To' date, 'Start time'", async ({ @@ -1307,8 +1322,8 @@ test.describe.skip("Daily Data page", () => { // Apply button is enabled await applyButton(page).click(); await page - .locator(".usa-table tbody") - .waitFor({ state: "visible" }); + .getByTestId("filter-status") + .waitFor({ timeout: 3000 }); // Form values persist await expect(startDate(page)).toHaveValue(fromDate); @@ -1318,13 +1333,13 @@ test.describe.skip("Daily Data page", () => { ); // Check filter status lists receiver value - const filterStatusText = filterStatus(page, [ - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${defaultStartTime}–${"11:59pm"}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // const filterStatusText = filterStatus(page, [ + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // `${defaultStartTime}–${"11:59pm"}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); }); test("with 'From' date, 'To' date, 'End time'", async ({ @@ -1337,8 +1352,8 @@ test.describe.skip("Daily Data page", () => { // Apply button is enabled await applyButton(page).click(); await page - .locator(".usa-table tbody") - .waitFor({ state: "visible" }); + .getByTestId("filter-status") + .waitFor({ timeout: 3000 }); // Form values persist await expect(startDate(page)).toHaveValue(fromDate); @@ -1348,16 +1363,16 @@ test.describe.skip("Daily Data page", () => { ); // Check filter status lists receiver value - const filterStatusText = filterStatus(page, [ - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - `${"12:00am"}–${defaultEndTime}`, - ]); - await expect( - page.getByTestId("filter-status"), - ).toContainText(filterStatusText); + // const filterStatusText = filterStatus(page, [ + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // `${"12:00am"}–${defaultEndTime}`, + // ]); + // await expect( + // page.getByTestId("filter-status"), + // ).toContainText(filterStatusText); }); - test("with 'From' date, 'To' date, 'Start time', 'End time'", async ({ + test.skip("with 'From' date, 'To' date, 'Start time', 'End time'", async ({ page, }) => { const fromDate = await setDate(page, "#start-date", 14); @@ -1370,6 +1385,9 @@ test.describe.skip("Daily Data page", () => { await page .locator(".usa-table tbody") .waitFor({ state: "visible" }); + await page + .getByTestId("filter-status") + .waitFor({ timeout: 3000 }); // Only needed when using live data // Check that table data contains the dates/times that were selected @@ -1412,21 +1430,24 @@ test.describe.skip("Daily Data page", () => { await searchInput(page).fill(reportId); await searchButton(page).click(); - const rowCount = await tableRows(page).count(); - expect(rowCount).toEqual(1); + // TODO: uncomment code to use with live data + // const rowCount = await tableRows(page).count(); + // expect(rowCount).toEqual(1); // Check filter status lists receiver value - let filterStatusText = filterStatus(page, [reportId]); - await expect(page.getByTestId("filter-status")).toContainText( - filterStatusText, - ); + // const filterStatusText = filterStatus(page, [reportId]); + // await expect(page.getByTestId("filter-status")).toContainText( + // filterStatusText, + // ); // Perform search with filters selected await page .locator("#receiver-dropdown") .selectOption(TEST_ORG_AK_RECEIVER); - const fromDate = await setDate(page, "#start-date", 14); - const toDate = await setDate(page, "#end-date", 0); + // const fromDate = await setDate(page, "#start-date", 14); + // const toDate = await setDate(page, "#end-date", 0); + await setDate(page, "#start-date", 14); + await setDate(page, "#end-date", 0); await applyButton(page).click(); await page @@ -1434,13 +1455,13 @@ test.describe.skip("Daily Data page", () => { .waitFor({ state: "visible" }); // Check filter status lists receiver value - filterStatusText = filterStatus(page, [ - TEST_ORG_AK_RECEIVER, - `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, - ]); - await expect(page.getByTestId("filter-status")).toContainText( - filterStatusText, - ); + // filterStatusText = filterStatus(page, [ + // TEST_ORG_AK_RECEIVER, + // `${format(fromDate, "MM/dd/yyyy")}–${format(toDate, "MM/dd/yyyy")}`, + // ]); + // await expect(page.getByTestId("filter-status")).toContainText( + // filterStatusText, + // ); // Check search is cleared await expect(searchInput(page)).toHaveValue(""); @@ -1459,7 +1480,7 @@ test.describe.skip("Daily Data page", () => { }); test.describe("search", () => { - test("returns match for Report ID", async ({ page }) => { + test.skip("returns match for Report ID", async ({ page }) => { const reportId = await tableDataCellValue(page, 0, 0); await searchInput(page).fill(reportId); await searchButton(page).click(); @@ -1477,7 +1498,7 @@ test.describe.skip("Daily Data page", () => { expect(await tableDataCellValue(page, 0, 0)).toEqual(reportId); }); - test("returns match for Filename", async ({ page }) => { + test.skip("returns match for Filename", async ({ page }) => { const fileName = await tableDataCellValue(page, 0, 4); await searchInput(page).fill(fileName); await searchButton(page).click(); @@ -1534,9 +1555,6 @@ test.describe.skip("Daily Data page", () => { await searchInput(page).fill(reportId); await searchButton(page).click(); - const rowCount = await tableRows(page).count(); - expect(rowCount).toEqual(9); - // Check filter status lists receiver value filterStatusText = filterStatus(page, [reportId]); await expect(page.getByTestId("filter-status")).toContainText( @@ -1558,9 +1576,7 @@ test.describe.skip("Daily Data page", () => { }); test("has pagination", async ({ page }) => { - await expect( - page.getByTestId("Deliveries pagination"), - ).toBeAttached(); + await expect(page.getByTestId("Pagination")).toBeAttached(); }); }); }); diff --git a/frontend-react/package.json b/frontend-react/package.json index a522d9d847c..ce2a80d4e2f 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -5,8 +5,8 @@ "type": "module", "npmClient": "yarn", "dependencies": { - "@microsoft/applicationinsights-react-js": "^17.1.1", - "@microsoft/applicationinsights-web": "^3.1.1", + "@microsoft/applicationinsights-react-js": "^17.1.2", + "@microsoft/applicationinsights-web": "^3.2.0", "@okta/okta-react": "^6.8.0", "@okta/okta-signin-widget": "^7.17.2", "@rest-hooks/rest": "^3.0.3", @@ -141,8 +141,8 @@ "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "@vitejs/plugin-react": "^4.2.1", - "@vitest/coverage-istanbul": "^1.4.0", - "@vitest/ui": "^1.4.0", + "@vitest/coverage-istanbul": "^1.5.3", + "@vitest/ui": "^1.5.3", "autoprefixer": "^10.4.19", "browserslist": "^4.23.0", "browserslist-useragent-regexp": "^4.1.3", @@ -161,7 +161,7 @@ "eslint-plugin-react-refresh": "^0.4.6", "eslint-plugin-storybook": "^0.8.0", "eslint-plugin-testing-library": "^6.2.2", - "eslint-plugin-vitest": "^0.4.1", + "eslint-plugin-vitest": "^0.5.4", "husky": "^9.0.11", "jsdom": "^24.0.0", "lint-staged": "^15.2.2", @@ -184,10 +184,10 @@ "tslib": "^2.6.2", "typescript": "^5.4.5", "undici": "^6.15.0", - "vite": "^5.2.8", + "vite": "^5.2.10", "vite-plugin-checker": "^0.6.4", "vite-plugin-svgr": "^4.2.0", - "vitest": "^1.4.0" + "vitest": "^1.5.3" }, "resolutions": { "@types/react": "18.3.1", diff --git a/frontend-react/yarn.lock b/frontend-react/yarn.lock index 006592416f5..8b3c184f262 100644 --- a/frontend-react/yarn.lock +++ b/frontend-react/yarn.lock @@ -1979,118 +1979,118 @@ __metadata: languageName: node linkType: hard -"@microsoft/applicationinsights-analytics-js@npm:3.1.1": - version: 3.1.1 - resolution: "@microsoft/applicationinsights-analytics-js@npm:3.1.1" +"@microsoft/applicationinsights-analytics-js@npm:3.2.0": + version: 3.2.0 + resolution: "@microsoft/applicationinsights-analytics-js@npm:3.2.0" dependencies: - "@microsoft/applicationinsights-common": 3.1.1 - "@microsoft/applicationinsights-core-js": 3.1.1 + "@microsoft/applicationinsights-common": 3.2.0 + "@microsoft/applicationinsights-core-js": 3.2.0 "@microsoft/applicationinsights-shims": 3.0.1 "@microsoft/dynamicproto-js": ^2.0.3 - "@nevware21/ts-utils": ">= 0.10.5 < 2.x" + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" peerDependencies: tslib: "*" - checksum: 9aa14b07e00ba31bcaacd9d4a53857624ae3be4e5f07a349c216522857bec96ce59b18a8f41fd1d15c640a5ee2fae3e97fc90a09337e547bb29cc9e791d77551 + checksum: b2516b7902b40b9f01f1e8b05696a66aec797dbbc6d1a44943a339672ccd3451ec0f4004430632447fc64cee30c18f1b59e7bd7f5c9d1196413821d6a125371c languageName: node linkType: hard -"@microsoft/applicationinsights-cfgsync-js@npm:3.1.1": - version: 3.1.1 - resolution: "@microsoft/applicationinsights-cfgsync-js@npm:3.1.1" +"@microsoft/applicationinsights-cfgsync-js@npm:3.2.0": + version: 3.2.0 + resolution: "@microsoft/applicationinsights-cfgsync-js@npm:3.2.0" dependencies: - "@microsoft/applicationinsights-common": 3.1.1 - "@microsoft/applicationinsights-core-js": 3.1.1 + "@microsoft/applicationinsights-common": 3.2.0 + "@microsoft/applicationinsights-core-js": 3.2.0 "@microsoft/applicationinsights-shims": 3.0.1 "@microsoft/dynamicproto-js": ^2.0.3 - "@nevware21/ts-async": ">= 0.3.0 < 2.x" - "@nevware21/ts-utils": ">= 0.10.5 < 2.x" + "@nevware21/ts-async": ">= 0.5.1 < 2.x" + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" peerDependencies: tslib: "*" - checksum: d2cf2c938fa84c91973315fe753b8c8fd7aa2cc63cbdef3a94f729f66c681e3e88f57f47122c78acadf87cbf97aa52666ade99124f1aad7071b4d3cdce8e81d7 + checksum: 3b5f36f6a69fce5ec4be0f1af0e5124d8530c90d53a6d476e25c6d2d575f996bcacb0b3e803b93167d572056835c1aeb51c7e442a403de6a9e7f0f75b73a73f0 languageName: node linkType: hard -"@microsoft/applicationinsights-channel-js@npm:3.1.1": - version: 3.1.1 - resolution: "@microsoft/applicationinsights-channel-js@npm:3.1.1" +"@microsoft/applicationinsights-channel-js@npm:3.2.0": + version: 3.2.0 + resolution: "@microsoft/applicationinsights-channel-js@npm:3.2.0" dependencies: - "@microsoft/applicationinsights-common": 3.1.1 - "@microsoft/applicationinsights-core-js": 3.1.1 + "@microsoft/applicationinsights-common": 3.2.0 + "@microsoft/applicationinsights-core-js": 3.2.0 "@microsoft/applicationinsights-shims": 3.0.1 "@microsoft/dynamicproto-js": ^2.0.3 - "@nevware21/ts-async": ">= 0.3.0 < 2.x" - "@nevware21/ts-utils": ">= 0.10.5 < 2.x" + "@nevware21/ts-async": ">= 0.5.1 < 2.x" + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" peerDependencies: tslib: "*" - checksum: 9816d41d582659cafbb751762c699737b87c42c4dc3b6a15381fab7a10139138dd6b15ff349721263889a347c74c9f847d5cae8a5ec13e3438285067ed142444 + checksum: 3e83ff3e9698fe2f3d48768b09375d6e16a5073447eb18a0f35e9c7ca98875a042fa3497ddaf6ba8cb1b4bae2bbe3af8c719392ea7827fa4e60f60f6490b1848 languageName: node linkType: hard -"@microsoft/applicationinsights-common@npm:3.1.1, @microsoft/applicationinsights-common@npm:^3.1.1": - version: 3.1.1 - resolution: "@microsoft/applicationinsights-common@npm:3.1.1" +"@microsoft/applicationinsights-common@npm:3.2.0, @microsoft/applicationinsights-common@npm:^3.1.2": + version: 3.2.0 + resolution: "@microsoft/applicationinsights-common@npm:3.2.0" dependencies: - "@microsoft/applicationinsights-core-js": 3.1.1 + "@microsoft/applicationinsights-core-js": 3.2.0 "@microsoft/applicationinsights-shims": 3.0.1 "@microsoft/dynamicproto-js": ^2.0.3 - "@nevware21/ts-utils": ">= 0.10.5 < 2.x" + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" peerDependencies: tslib: "*" - checksum: b3e845335018a7104f74fd1c4308d481f5abc8be3a3eb3936f032663dc25d5edce1d0b72d3174f8b8fdc455cccff69f0012f2da32dd5e04e8c0e5db3b5accffa + checksum: 10e5b9004bfe5030909e7a3e10d93619c337b435c194e6516a80fa76ecf17385fe986ad83e957276e0568652f2e9e31d17d4f3a74e2ccff01a77ea911b652d1a languageName: node linkType: hard -"@microsoft/applicationinsights-core-js@npm:3.1.1, @microsoft/applicationinsights-core-js@npm:^3.1.1": - version: 3.1.1 - resolution: "@microsoft/applicationinsights-core-js@npm:3.1.1" +"@microsoft/applicationinsights-core-js@npm:3.2.0, @microsoft/applicationinsights-core-js@npm:^3.1.2": + version: 3.2.0 + resolution: "@microsoft/applicationinsights-core-js@npm:3.2.0" dependencies: "@microsoft/applicationinsights-shims": 3.0.1 "@microsoft/dynamicproto-js": ^2.0.3 - "@nevware21/ts-async": ">= 0.3.0 < 2.x" - "@nevware21/ts-utils": ">= 0.10.5 < 2.x" + "@nevware21/ts-async": ">= 0.5.1 < 2.x" + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" peerDependencies: tslib: "*" - checksum: 6534cfa7fbafe4980937875f79f8ad25d90595b08d8f95430a5bcafe9ed23a3f4fcf749496382303e77dcb41d2bb8b4f9254ad13c3997da9d403ba74751c7a5e + checksum: 5411baea15796c85b2cc0dadc76947b5d75e1bceb08efa81d705442a4774848ffb398a432eabf22084820122b1a568b9b766ab0e3d9c0debf28acac03cf4b738 languageName: node linkType: hard -"@microsoft/applicationinsights-dependencies-js@npm:3.1.1": - version: 3.1.1 - resolution: "@microsoft/applicationinsights-dependencies-js@npm:3.1.1" +"@microsoft/applicationinsights-dependencies-js@npm:3.2.0": + version: 3.2.0 + resolution: "@microsoft/applicationinsights-dependencies-js@npm:3.2.0" dependencies: - "@microsoft/applicationinsights-common": 3.1.1 - "@microsoft/applicationinsights-core-js": 3.1.1 + "@microsoft/applicationinsights-common": 3.2.0 + "@microsoft/applicationinsights-core-js": 3.2.0 "@microsoft/applicationinsights-shims": 3.0.1 "@microsoft/dynamicproto-js": ^2.0.3 - "@nevware21/ts-async": ">= 0.3.0 < 2.x" - "@nevware21/ts-utils": ">= 0.10.5 < 2.x" + "@nevware21/ts-async": ">= 0.5.1 < 2.x" + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" peerDependencies: tslib: "*" - checksum: 13683ac5464cb35fc84a2ad639aa463965ab5c5aac34f043cce78539b041b24c2088ab4fbb57f4b6272746763d2b7ef362c493dbd5663ef87a07cb0f2bab2753 + checksum: 6db535296ac4474022d43e293c7664e0b035b640f9d3ec0e94b125cda83dd1c3e99d81d15004fff8b70a6db04e91c565710c2a904ed6eed81be3f63b18ecd4a5 languageName: node linkType: hard -"@microsoft/applicationinsights-properties-js@npm:3.1.1": - version: 3.1.1 - resolution: "@microsoft/applicationinsights-properties-js@npm:3.1.1" +"@microsoft/applicationinsights-properties-js@npm:3.2.0": + version: 3.2.0 + resolution: "@microsoft/applicationinsights-properties-js@npm:3.2.0" dependencies: - "@microsoft/applicationinsights-common": 3.1.1 - "@microsoft/applicationinsights-core-js": 3.1.1 + "@microsoft/applicationinsights-common": 3.2.0 + "@microsoft/applicationinsights-core-js": 3.2.0 "@microsoft/applicationinsights-shims": 3.0.1 "@microsoft/dynamicproto-js": ^2.0.3 - "@nevware21/ts-utils": ">= 0.10.5 < 2.x" + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" peerDependencies: tslib: "*" - checksum: ec108bb9537379cc471d73c2bfe04e40fbccb935d373fd0a72d9c2cbdca541b5570c42ecc924280bea7f56297b1316a1262fda400bfbaed5b9f889cb0d581455 + checksum: f72655e4383c664c8fa311e75ac8af8866fa6a3113322268385474a34cb111c94979b623e10f4df493162a90a1d2fe443165dc9a97edeca0d306e868df5fad93 languageName: node linkType: hard -"@microsoft/applicationinsights-react-js@npm:^17.1.1": - version: 17.1.1 - resolution: "@microsoft/applicationinsights-react-js@npm:17.1.1" +"@microsoft/applicationinsights-react-js@npm:^17.1.2": + version: 17.1.2 + resolution: "@microsoft/applicationinsights-react-js@npm:17.1.2" dependencies: - "@microsoft/applicationinsights-common": ^3.1.1 - "@microsoft/applicationinsights-core-js": ^3.1.1 + "@microsoft/applicationinsights-common": ^3.1.2 + "@microsoft/applicationinsights-core-js": ^3.1.2 "@microsoft/applicationinsights-shims": ^3.0.1 "@microsoft/dynamicproto-js": ^2.0.3 "@nevware21/ts-utils": ">= 0.10.5 < 2.x" @@ -2098,7 +2098,7 @@ __metadata: history: ">= 4.10.1" react: ">= 17.0.1" tslib: "*" - checksum: aa5862706a6c8992bc6b490f7b7467b962b26f330b65a54364f59d14ef38b5d831613d38135e4d0d3e18a445bbf12022e878798060cfc422548e66f7d96e6197 + checksum: 87e5cbe222024f643783ef76a63a3e44c5e3f92e15f0b08c1d17dd99c1055d7857f1cc4c55640e0a09a4ee455eb08ea937e02c05daac0c1c88fd6d41adb44667 languageName: node linkType: hard @@ -2111,24 +2111,24 @@ __metadata: languageName: node linkType: hard -"@microsoft/applicationinsights-web@npm:^3.1.1": - version: 3.1.1 - resolution: "@microsoft/applicationinsights-web@npm:3.1.1" - dependencies: - "@microsoft/applicationinsights-analytics-js": 3.1.1 - "@microsoft/applicationinsights-cfgsync-js": 3.1.1 - "@microsoft/applicationinsights-channel-js": 3.1.1 - "@microsoft/applicationinsights-common": 3.1.1 - "@microsoft/applicationinsights-core-js": 3.1.1 - "@microsoft/applicationinsights-dependencies-js": 3.1.1 - "@microsoft/applicationinsights-properties-js": 3.1.1 +"@microsoft/applicationinsights-web@npm:^3.2.0": + version: 3.2.0 + resolution: "@microsoft/applicationinsights-web@npm:3.2.0" + dependencies: + "@microsoft/applicationinsights-analytics-js": 3.2.0 + "@microsoft/applicationinsights-cfgsync-js": 3.2.0 + "@microsoft/applicationinsights-channel-js": 3.2.0 + "@microsoft/applicationinsights-common": 3.2.0 + "@microsoft/applicationinsights-core-js": 3.2.0 + "@microsoft/applicationinsights-dependencies-js": 3.2.0 + "@microsoft/applicationinsights-properties-js": 3.2.0 "@microsoft/applicationinsights-shims": 3.0.1 "@microsoft/dynamicproto-js": ^2.0.3 - "@nevware21/ts-async": ">= 0.3.0 < 2.x" - "@nevware21/ts-utils": ">= 0.10.5 < 2.x" + "@nevware21/ts-async": ">= 0.5.1 < 2.x" + "@nevware21/ts-utils": ">= 0.11.1 < 2.x" peerDependencies: tslib: "*" - checksum: 94032960f8421e92152e614fec6d846c5dc515b6ef07ea7f6c4da26e2b4524fdd72a0ca176dcf70dff2e3016af24f14d98281e82cf40bb57c00bcf441506be45 + checksum: 9d6659e4277398e0e538dfe385360904084406afeccf4128c037807e9ec747fe99c35369b66f9e4ab2414eaf0d0828fdce0847103d98509d16db5d9f47828e00 languageName: node linkType: hard @@ -2173,22 +2173,29 @@ __metadata: languageName: node linkType: hard -"@nevware21/ts-async@npm:>= 0.3.0 < 2.x": - version: 0.3.0 - resolution: "@nevware21/ts-async@npm:0.3.0" +"@nevware21/ts-async@npm:>= 0.5.1 < 2.x": + version: 0.5.1 + resolution: "@nevware21/ts-async@npm:0.5.1" dependencies: - "@nevware21/ts-utils": ">= 0.10.0 < 2.x" - checksum: 4380f5af6ad9f2ea0cb287c620a062688d33de80e5c39727a418916a97e838340738971d9c252b6cac5becb071e1ff89c69c538a13c6ea5df4b65bee1c4b030e + "@nevware21/ts-utils": ">= 0.11.2 < 2.x" + checksum: b53f53f4807b6fab1894b55297deabb46a24f7d6dbca9cf7226805c345824b28fce2ba280d50408d6a313fccf5c7da29be3fbcbeae037cfedcce10af2d44a4f8 languageName: node linkType: hard -"@nevware21/ts-utils@npm:>= 0.10.0 < 2.x, @nevware21/ts-utils@npm:>= 0.10.4 < 2.x, @nevware21/ts-utils@npm:>= 0.10.5 < 2.x, @nevware21/ts-utils@npm:>= 0.9.4 < 2.x": +"@nevware21/ts-utils@npm:>= 0.10.4 < 2.x, @nevware21/ts-utils@npm:>= 0.10.5 < 2.x, @nevware21/ts-utils@npm:>= 0.9.4 < 2.x": version: 0.10.5 resolution: "@nevware21/ts-utils@npm:0.10.5" checksum: a36294c836a7fdfd82acbaddd6940d48225263613e04624c8d523722ee2cc99fa776cdcfe95d9f86d6df89e351549ffd610375345da0d7bc7af5e2668e69d971 languageName: node linkType: hard +"@nevware21/ts-utils@npm:>= 0.11.1 < 2.x, @nevware21/ts-utils@npm:>= 0.11.2 < 2.x": + version: 0.11.2 + resolution: "@nevware21/ts-utils@npm:0.11.2" + checksum: f6ef45341705da44502105663c0c6106af452bf520e0a43fc1c479b021f987bba4b5560d6b5153358a33cf51fe23299994317ef078b0dd5dc509ec3d3977b23e + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4430,16 +4437,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:7.6.0": - version: 7.6.0 - resolution: "@typescript-eslint/scope-manager@npm:7.6.0" - dependencies: - "@typescript-eslint/types": 7.6.0 - "@typescript-eslint/visitor-keys": 7.6.0 - checksum: 07c0215bb9631dc580801d860b90d30bf5815bd3acfa1f50d7d7c28625ec90b6c74076d352a1f4d3b2043345b7c2f72575cee7cfc9ad8dcb3d6c8f1ddc7bc2f3 - languageName: node - linkType: hard - "@typescript-eslint/scope-manager@npm:7.8.0": version: 7.8.0 resolution: "@typescript-eslint/scope-manager@npm:7.8.0" @@ -4474,13 +4471,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:7.6.0": - version: 7.6.0 - resolution: "@typescript-eslint/types@npm:7.6.0" - checksum: 5e3157ff8d24e9dd384ed189e89e920bb2525c95330e9f87a9f9fc65c12ab4a2e701b380ede5a655dc4c4517ae27f1b9ca6b3cfb3dada02f03dd634559f06ea1 - languageName: node - linkType: hard - "@typescript-eslint/types@npm:7.8.0": version: 7.8.0 resolution: "@typescript-eslint/types@npm:7.8.0" @@ -4506,25 +4496,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:7.6.0": - version: 7.6.0 - resolution: "@typescript-eslint/typescript-estree@npm:7.6.0" - dependencies: - "@typescript-eslint/types": 7.6.0 - "@typescript-eslint/visitor-keys": 7.6.0 - debug: ^4.3.4 - globby: ^11.1.0 - is-glob: ^4.0.3 - minimatch: ^9.0.4 - semver: ^7.6.0 - ts-api-utils: ^1.3.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 8a19895fc58de1094787e03b31a4a7fe96728e1071b0ddfbaeaecfdb15aca3cdc38944ca27a9c3ea0a5304c0c5339f587e67e97f958c1cbb7678c4f99c91d58d - languageName: node - linkType: hard - "@typescript-eslint/typescript-estree@npm:7.8.0": version: 7.8.0 resolution: "@typescript-eslint/typescript-estree@npm:7.8.0" @@ -4544,7 +4515,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:7.8.0": +"@typescript-eslint/utils@npm:7.8.0, @typescript-eslint/utils@npm:^7.7.1": version: 7.8.0 resolution: "@typescript-eslint/utils@npm:7.8.0" dependencies: @@ -4579,23 +4550,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:^7.4.0": - version: 7.6.0 - resolution: "@typescript-eslint/utils@npm:7.6.0" - dependencies: - "@eslint-community/eslint-utils": ^4.4.0 - "@types/json-schema": ^7.0.15 - "@types/semver": ^7.5.8 - "@typescript-eslint/scope-manager": 7.6.0 - "@typescript-eslint/types": 7.6.0 - "@typescript-eslint/typescript-estree": 7.6.0 - semver: ^7.6.0 - peerDependencies: - eslint: ^8.56.0 - checksum: 22475182efee7b19b18d6aee2fe7dbac6303c9b41d99b3323a6cb295bf2cd3505ac2ebc6a0acffccbd8af2917ba42c055404bbed9687af760badd5f0bdbc17b8 - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -4606,16 +4560,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:7.6.0": - version: 7.6.0 - resolution: "@typescript-eslint/visitor-keys@npm:7.6.0" - dependencies: - "@typescript-eslint/types": 7.6.0 - eslint-visitor-keys: ^3.4.3 - checksum: 9f0378435636a6a80dfcbedfcdf5fd43f200628e0359879f2d8b7ed0b73563e727ad356f7fe9c07d5e09a642c45927b1380a8418a60b8169f7ae749f59f14b9a - languageName: node - linkType: hard - "@typescript-eslint/visitor-keys@npm:7.8.0": version: 7.8.0 resolution: "@typescript-eslint/visitor-keys@npm:7.8.0" @@ -4660,9 +4604,9 @@ __metadata: languageName: node linkType: hard -"@vitest/coverage-istanbul@npm:^1.4.0": - version: 1.4.0 - resolution: "@vitest/coverage-istanbul@npm:1.4.0" +"@vitest/coverage-istanbul@npm:^1.5.3": + version: 1.5.3 + resolution: "@vitest/coverage-istanbul@npm:1.5.3" dependencies: debug: ^4.3.4 istanbul-lib-coverage: ^3.2.2 @@ -4674,8 +4618,8 @@ __metadata: picocolors: ^1.0.0 test-exclude: ^6.0.0 peerDependencies: - vitest: 1.4.0 - checksum: 270a33445e62c6921dfe2bcf7b355bc1f5c2c5d7d1ca561cd1f1d02a8b4925c47b1290af7aeb015f980b26b00e93ad0f5343e374b0e1f51a3faf998384203934 + vitest: 1.5.3 + checksum: 20bb267faf2b6583e567ed9c733299bae73054c2fb5b3ef0251ad800ac56878fca6a6754f09b4b980bf2f12099bf61cb45a35ccc589e0816620497b08b45925f languageName: node linkType: hard @@ -4690,36 +4634,36 @@ __metadata: languageName: node linkType: hard -"@vitest/expect@npm:1.4.0": - version: 1.4.0 - resolution: "@vitest/expect@npm:1.4.0" +"@vitest/expect@npm:1.5.3": + version: 1.5.3 + resolution: "@vitest/expect@npm:1.5.3" dependencies: - "@vitest/spy": 1.4.0 - "@vitest/utils": 1.4.0 + "@vitest/spy": 1.5.3 + "@vitest/utils": 1.5.3 chai: ^4.3.10 - checksum: aca8b0b592bc020febb08767a230a2f4118453904972df06b2a34c76a669e8eb260ddfd8b0cdc152e93dbf21e0d7ae98bdf09fa80477b21145ac21c01fc5a0d3 + checksum: b273ffc229ddbfa91fef6e48e170e0f192f7989a6c59db182819b2b5885268d910578899b242818a18142bc261074a60536212f1aaa9f20b6f91f9e4b81e1443 languageName: node linkType: hard -"@vitest/runner@npm:1.4.0": - version: 1.4.0 - resolution: "@vitest/runner@npm:1.4.0" +"@vitest/runner@npm:1.5.3": + version: 1.5.3 + resolution: "@vitest/runner@npm:1.5.3" dependencies: - "@vitest/utils": 1.4.0 + "@vitest/utils": 1.5.3 p-limit: ^5.0.0 pathe: ^1.1.1 - checksum: 41a847d1ba916c64e482a69342222a40b81014ad06da7ccb7d8cd4119c5718564c22b00367574803642e6ca6ab5678509073b5c460bedc2b385d4e990351012f + checksum: b7e5193e3ea967cd9edb96b7db3cc0addea46e3c1a47908ca4ff0ccd31f69f021beda69bcbf3fff42ad59504aa49b946b1b05529994fba5112e3b023a5d61bd6 languageName: node linkType: hard -"@vitest/snapshot@npm:1.4.0": - version: 1.4.0 - resolution: "@vitest/snapshot@npm:1.4.0" +"@vitest/snapshot@npm:1.5.3": + version: 1.5.3 + resolution: "@vitest/snapshot@npm:1.5.3" dependencies: magic-string: ^0.30.5 pathe: ^1.1.1 pretty-format: ^29.7.0 - checksum: fe495661d682534b41f3ac373c017ac84c04263087a715307715bb41a69c307d6f237b18cc8195bc22d82bf38a1a1a773b0c99715d5de5f40c6169e916b8f4a4 + checksum: 34ff60f412dd41fff56a4001ceb7b37865d89914fba47e12eb661c776eb3f37345459de3f451a8e94decae7c122fc7d59e879bd8b956bdbfaead1f581a3ef259 languageName: node linkType: hard @@ -4732,7 +4676,16 @@ __metadata: languageName: node linkType: hard -"@vitest/spy@npm:1.4.0, @vitest/spy@npm:^1.3.1": +"@vitest/spy@npm:1.5.3": + version: 1.5.3 + resolution: "@vitest/spy@npm:1.5.3" + dependencies: + tinyspy: ^2.2.0 + checksum: 2219d248f3bb1679ab592e7432cabd91588041eb5ab8f978fda57fc371cb763fed8760bd5b09d26bb9fae680ae61592a45a72ce49f77b94668c822bfed97f4a6 + languageName: node + linkType: hard + +"@vitest/spy@npm:^1.3.1": version: 1.4.0 resolution: "@vitest/spy@npm:1.4.0" dependencies: @@ -4741,11 +4694,11 @@ __metadata: languageName: node linkType: hard -"@vitest/ui@npm:^1.4.0": - version: 1.4.0 - resolution: "@vitest/ui@npm:1.4.0" +"@vitest/ui@npm:^1.5.3": + version: 1.5.3 + resolution: "@vitest/ui@npm:1.5.3" dependencies: - "@vitest/utils": 1.4.0 + "@vitest/utils": 1.5.3 fast-glob: ^3.3.2 fflate: ^0.8.1 flatted: ^3.2.9 @@ -4753,8 +4706,8 @@ __metadata: picocolors: ^1.0.0 sirv: ^2.0.4 peerDependencies: - vitest: 1.4.0 - checksum: 5a508fd80fcdf87acf4697ae8836cf4dcdd5300fd1574a400a62f5242a6bb4028b3002d60db20f84c3e3e4b4ae110b33194c31219330fffe205e5d9fbf1c65c8 + vitest: 1.5.3 + checksum: 00a67f822ce56e86de6c657a8c4394e34ffef2ae109be6b8fba823a213b60e31d3000273c09703ebf4caf702d7b98897725d5f96de2f616fae82aa03129395ca languageName: node linkType: hard @@ -4770,7 +4723,19 @@ __metadata: languageName: node linkType: hard -"@vitest/utils@npm:1.4.0, @vitest/utils@npm:^1.3.1": +"@vitest/utils@npm:1.5.3": + version: 1.5.3 + resolution: "@vitest/utils@npm:1.5.3" + dependencies: + diff-sequences: ^29.6.3 + estree-walker: ^3.0.3 + loupe: ^2.3.7 + pretty-format: ^29.7.0 + checksum: 17152b8e6f6d2ec61970637df9b545f4b96a0acae1d4e1bc1e7c6b3e3c6f59f61b9f898e243ed44a1b9429433a7a8cd3add7a14bcd63e2bfabf00447e8a5d751 + languageName: node + linkType: hard + +"@vitest/utils@npm:^1.3.1": version: 1.4.0 resolution: "@vitest/utils@npm:1.4.0" dependencies: @@ -7441,20 +7406,20 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-vitest@npm:^0.4.1": - version: 0.4.1 - resolution: "eslint-plugin-vitest@npm:0.4.1" +"eslint-plugin-vitest@npm:^0.5.4": + version: 0.5.4 + resolution: "eslint-plugin-vitest@npm:0.5.4" dependencies: - "@typescript-eslint/utils": ^7.4.0 + "@typescript-eslint/utils": ^7.7.1 peerDependencies: - eslint: ">=8.0.0" + eslint: ^8.57.0 || ^9.0.0 vitest: "*" peerDependenciesMeta: "@typescript-eslint/eslint-plugin": optional: true vitest: optional: true - checksum: d1b160d45901f81be7c9518e0542e8424afff099ae755ff885d3b560adf357e09f9fd910ce6cb4c72572399ab5564fc4b3a43405a5b5a019e6aa6fa01995dc72 + checksum: 5995bccf9184914428070ef6d69e310a1e4b44e2b5ac4842433f034543e1fff175ae32221c31405e4335bb20cea34b0b3494354f83b0d3f4d3199e0f1e16ac16 languageName: node linkType: hard @@ -12482,8 +12447,8 @@ __metadata: dependencies: "@mdx-js/react": ^3.0.1 "@mdx-js/rollup": ^3.0.1 - "@microsoft/applicationinsights-react-js": ^17.1.1 - "@microsoft/applicationinsights-web": ^3.1.1 + "@microsoft/applicationinsights-react-js": ^17.1.2 + "@microsoft/applicationinsights-web": ^3.2.0 "@okta/okta-react": ^6.8.0 "@okta/okta-signin-widget": ^7.17.2 "@playwright/test": ^1.43.1 @@ -12526,8 +12491,8 @@ __metadata: "@typescript-eslint/parser": ^7.8.0 "@uswds/uswds": 3.7.1 "@vitejs/plugin-react": ^4.2.1 - "@vitest/coverage-istanbul": ^1.4.0 - "@vitest/ui": ^1.4.0 + "@vitest/coverage-istanbul": ^1.5.3 + "@vitest/ui": ^1.5.3 autoprefixer: ^10.4.19 axios: ^1.6.8 browserslist: ^4.23.0 @@ -12552,7 +12517,7 @@ __metadata: eslint-plugin-react-refresh: ^0.4.6 eslint-plugin-storybook: ^0.8.0 eslint-plugin-testing-library: ^6.2.2 - eslint-plugin-vitest: ^0.4.1 + eslint-plugin-vitest: ^0.5.4 export-to-csv-fix-source-map: ^0.2.1 focus-trap-react: ^10.2.3 history: ^5.3.0 @@ -12597,10 +12562,10 @@ __metadata: undici: ^6.15.0 use-deep-compare-effect: ^1.8.1 uuid: ^9.0.1 - vite: ^5.2.8 + vite: ^5.2.10 vite-plugin-checker: ^0.6.4 vite-plugin-svgr: ^4.2.0 - vitest: ^1.4.0 + vitest: ^1.5.3 web-vitals: ^3.4.0 languageName: unknown linkType: soft @@ -14378,10 +14343,10 @@ __metadata: languageName: node linkType: hard -"tinypool@npm:^0.8.2": - version: 0.8.3 - resolution: "tinypool@npm:0.8.3" - checksum: 61ba2514b19db23a67920055b27a613a70b1a65eccd25a40d0a87c83506667bdf228bb51443042fe04ab79c92e1e6777cfe4624d318a8b0d2acc346f4cf713ee +"tinypool@npm:^0.8.3": + version: 0.8.4 + resolution: "tinypool@npm:0.8.4" + checksum: d40c40e062d5eeae85dadc39294dde6bc7b9a7a7cf0c972acbbe5a2b42491dfd4c48381c1e48bbe02aff4890e63de73d115b2e7de2ce4c81356aa5e654a43caf languageName: node linkType: hard @@ -15215,9 +15180,9 @@ __metadata: languageName: node linkType: hard -"vite-node@npm:1.4.0": - version: 1.4.0 - resolution: "vite-node@npm:1.4.0" +"vite-node@npm:1.5.3": + version: 1.5.3 + resolution: "vite-node@npm:1.5.3" dependencies: cac: ^6.7.14 debug: ^4.3.4 @@ -15226,7 +15191,7 @@ __metadata: vite: ^5.0.0 bin: vite-node: vite-node.mjs - checksum: 1abbeac935a5e1e3b6161974ae28b6a3ec88766b06bc082ab3f2883345ee74f5643f3004df1c7808c2b52d445ffbd067bb79a1a3920c2eaa7ca8084b11b7de46 + checksum: 6f3a851f5490ed8b7fa9ca2a2e6b2b0f6f74569dbd606b6437458792c3f0423deae1c9f5127a3d35c6d47b3f32dce459c406dd59f5cb9f59b52f86287b73e153 languageName: node linkType: hard @@ -15293,7 +15258,7 @@ __metadata: languageName: node linkType: hard -"vite@npm:^5.0.0, vite@npm:^5.2.8": +"vite@npm:^5.0.0": version: 5.2.8 resolution: "vite@npm:5.2.8" dependencies: @@ -15333,15 +15298,55 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^1.4.0": - version: 1.4.0 - resolution: "vitest@npm:1.4.0" +"vite@npm:^5.2.10": + version: 5.2.10 + resolution: "vite@npm:5.2.10" + dependencies: + esbuild: ^0.20.1 + fsevents: ~2.3.3 + postcss: ^8.4.38 + rollup: ^4.13.0 + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: feb11be2b97259783afe1d0ff4b7ee0bef78fe8934d8b043465e039e4d8318249e2263adc724b719b9e9b45a37e012caeb7746854768f371d1a82bb189f50fe6 + languageName: node + linkType: hard + +"vitest@npm:^1.5.3": + version: 1.5.3 + resolution: "vitest@npm:1.5.3" dependencies: - "@vitest/expect": 1.4.0 - "@vitest/runner": 1.4.0 - "@vitest/snapshot": 1.4.0 - "@vitest/spy": 1.4.0 - "@vitest/utils": 1.4.0 + "@vitest/expect": 1.5.3 + "@vitest/runner": 1.5.3 + "@vitest/snapshot": 1.5.3 + "@vitest/spy": 1.5.3 + "@vitest/utils": 1.5.3 acorn-walk: ^8.3.2 chai: ^4.3.10 debug: ^4.3.4 @@ -15353,15 +15358,15 @@ __metadata: std-env: ^3.5.0 strip-literal: ^2.0.0 tinybench: ^2.5.1 - tinypool: ^0.8.2 + tinypool: ^0.8.3 vite: ^5.0.0 - vite-node: 1.4.0 + vite-node: 1.5.3 why-is-node-running: ^2.2.2 peerDependencies: "@edge-runtime/vm": "*" "@types/node": ^18.0.0 || >=20.0.0 - "@vitest/browser": 1.4.0 - "@vitest/ui": 1.4.0 + "@vitest/browser": 1.5.3 + "@vitest/ui": 1.5.3 happy-dom: "*" jsdom: "*" peerDependenciesMeta: @@ -15379,7 +15384,7 @@ __metadata: optional: true bin: vitest: vitest.mjs - checksum: e7141c0ecc629c350d8c718051fb19219ca88a100d335fedbe481c4320f285380e3235316a69a330514c34663fcafa37c801151162d0a538e92821e7faad71a6 + checksum: 1cd965e34dcd9a3f4dead3b455470d1fc4c7aa09ca7a3e0b2493c85cf2d5e9913aace1ee41cb17caaada4e4ba3effa552a2ae5f3cc689b6899a5ef85e856e8c6 languageName: node linkType: hard diff --git a/operations/app/terraform/modules/function_app/locals.tf b/operations/app/terraform/modules/function_app/locals.tf index 12c8bc09fd0..f5e9caf2760 100644 --- a/operations/app/terraform/modules/function_app/locals.tf +++ b/operations/app/terraform/modules/function_app/locals.tf @@ -16,6 +16,7 @@ locals { "RS_OKTA_redirect" = var.RS_okta_redirect_url "RS_OKTA_authkey" = var.RS_OKTA_authKey "RS_OKTA_ClientId" = var.RS_OKTA_clientId + "ETOR_TI_baseurl" = var.etor_ti_base_url # Manage client secrets via a Key Vault "CREDENTIAL_STORAGE_METHOD" = "AZURE" "CREDENTIAL_KEY_VAULT_NAME" = "${var.resource_prefix}-clientconfig" diff --git a/operations/app/terraform/modules/function_app/~inputs.tf b/operations/app/terraform/modules/function_app/~inputs.tf index 3d3c80c8e39..9e0ac470df5 100644 --- a/operations/app/terraform/modules/function_app/~inputs.tf +++ b/operations/app/terraform/modules/function_app/~inputs.tf @@ -88,6 +88,7 @@ variable "RS_okta_base_url" {} variable "RS_OKTA_authKey" {} variable "RS_OKTA_clientId" {} variable "RS_OKTA_scope" {} +variable "etor_ti_base_url" {} variable "subnets" { description = "A set of all available subnet combinations" diff --git a/operations/app/terraform/vars/demo/locals.tf b/operations/app/terraform/vars/demo/locals.tf index 9dc73bbe6da..abf2e8b60d1 100644 --- a/operations/app/terraform/vars/demo/locals.tf +++ b/operations/app/terraform/vars/demo/locals.tf @@ -16,6 +16,7 @@ locals { RS_okta_base_url = "reportstream.oktapreview.com" RS_okta_redirect_url = "https://prime-data-hub-XXXXXXX.azurefd.net/download" RS_OKTA_scope = "reportstream_dev" + etor_ti_base_url = "https://cdcti-stg-api.azurewebsites.net" } key_vault = { app_config_kv_name = "pdh${local.init.environment}-appconfig${local.init.random_id}" diff --git a/operations/app/terraform/vars/demo/main.tf b/operations/app/terraform/vars/demo/main.tf index cb25ca9b75b..c6f45ad709f 100644 --- a/operations/app/terraform/vars/demo/main.tf +++ b/operations/app/terraform/vars/demo/main.tf @@ -181,6 +181,7 @@ module "function_app" { OKTA_authKey = data.azurerm_key_vault_secret.OKTA_authKey.value RS_OKTA_clientId = data.azurerm_key_vault_secret.RS_OKTA_clientId.value RS_OKTA_authKey = data.azurerm_key_vault_secret.RS_OKTA_authKey.value + etor_ti_base_url = local.init.etor_ti_base_url } module "front_door" { diff --git a/operations/app/terraform/vars/prod/locals.tf b/operations/app/terraform/vars/prod/locals.tf index 801f74b5ea3..2ffb2ce632a 100644 --- a/operations/app/terraform/vars/prod/locals.tf +++ b/operations/app/terraform/vars/prod/locals.tf @@ -15,6 +15,7 @@ locals { RS_OKTA_scope = "reportstream_prod" storage_queue_name = ["process"] sftp_container_module = false + etor_ti_base_url = "https://cdcti-prd-api.azurewebsites.net" } key_vault = { diff --git a/operations/app/terraform/vars/prod/main.tf b/operations/app/terraform/vars/prod/main.tf index d748f9587d3..2287d03d93c 100644 --- a/operations/app/terraform/vars/prod/main.tf +++ b/operations/app/terraform/vars/prod/main.tf @@ -159,6 +159,7 @@ module "function_app" { OKTA_authKey = data.azurerm_key_vault_secret.OKTA_authKey.value RS_OKTA_clientId = data.azurerm_key_vault_secret.RS_OKTA_clientId.value RS_OKTA_authKey = data.azurerm_key_vault_secret.RS_OKTA_authKey.value + etor_ti_base_url = local.init.etor_ti_base_url } module "front_door" { diff --git a/operations/app/terraform/vars/staging/locals.tf b/operations/app/terraform/vars/staging/locals.tf index 368a4b53db2..d681f4de435 100644 --- a/operations/app/terraform/vars/staging/locals.tf +++ b/operations/app/terraform/vars/staging/locals.tf @@ -15,6 +15,7 @@ locals { RS_OKTA_scope = "reportstream_dev" storage_queue_name = ["process", "batch", "batch-poison", "elr-fhir-convert", "process-poison", "send", "send-poison", "elr-fhir-convert", "elr-fhir-convert-poison", "elr-fhir-route", "elr-fhir-translate", "elr-fhir-translate-poison", "process-elr"] sftp_container_module = true + etor_ti_base_url = "https://cdcti-stg-api.azurewebsites.net" } key_vault = { app_config_kv_name = "pdh${local.init.environment}-appconfig" diff --git a/operations/app/terraform/vars/staging/main.tf b/operations/app/terraform/vars/staging/main.tf index 1e07b1ec093..f7652a146f2 100644 --- a/operations/app/terraform/vars/staging/main.tf +++ b/operations/app/terraform/vars/staging/main.tf @@ -158,6 +158,7 @@ module "function_app" { OKTA_authKey = data.azurerm_key_vault_secret.OKTA_authKey.value RS_OKTA_clientId = data.azurerm_key_vault_secret.RS_OKTA_clientId.value RS_OKTA_authKey = data.azurerm_key_vault_secret.RS_OKTA_authKey.value + etor_ti_base_url = local.init.etor_ti_base_url } module "front_door" { diff --git a/operations/app/terraform/vars/test/locals.tf b/operations/app/terraform/vars/test/locals.tf index 60c6a1decce..2ad7f5012eb 100644 --- a/operations/app/terraform/vars/test/locals.tf +++ b/operations/app/terraform/vars/test/locals.tf @@ -15,6 +15,7 @@ locals { RS_OKTA_scope = "reportstream_dev" storage_queue_name = ["process"] sftp_container_module = true + etor_ti_base_url = "https://cdcti-stg-api.azurewebsites.net" } key_vault = { app_config_kv_name = "pdh${local.init.environment}-app-config" diff --git a/operations/app/terraform/vars/test/main.tf b/operations/app/terraform/vars/test/main.tf index 84c54f8725a..1c74f9e57c6 100644 --- a/operations/app/terraform/vars/test/main.tf +++ b/operations/app/terraform/vars/test/main.tf @@ -162,6 +162,7 @@ module "function_app" { OKTA_authKey = data.azurerm_key_vault_secret.OKTA_authKey.value RS_OKTA_clientId = data.azurerm_key_vault_secret.RS_OKTA_clientId.value RS_OKTA_authKey = data.azurerm_key_vault_secret.RS_OKTA_authKey.value + etor_ti_base_url = local.init.etor_ti_base_url } module "front_door" { diff --git a/prime-router/build.gradle.kts b/prime-router/build.gradle.kts index 20752ea5586..03a7d3b458f 100644 --- a/prime-router/build.gradle.kts +++ b/prime-router/build.gradle.kts @@ -952,6 +952,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.2") testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.28.0") testImplementation("io.ktor:ktor-client-mock:$ktorVersion") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.2") testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") testImplementation("org.testcontainers:testcontainers:1.19.7") diff --git a/prime-router/docs/api/delivery.yml b/prime-router/docs/api/delivery.yml index add18b34fef..f6be75c81f7 100644 --- a/prime-router/docs/api/delivery.yml +++ b/prime-router/docs/api/delivery.yml @@ -41,6 +41,29 @@ paths: description: The search request could not be parsed '404': description: The receiver could not be found + /waters/report/{reportId}/delivery/etorMetadata: + get: + summary: Retrieves ETOR metadata for a recipient's report + description: Fetches the ETOR intermediary's metadata given the recipient's report ID. + operationId: getEtorMetadataForDelivery + parameters: + - name: reportId + in: path + required: true + description: The unique identifier of the report. + schema: + type: string + responses: + '200': + description: Successful retrieval of ETOR metadata. + content: + application/json: + schema: + $ref: 'https://github.com/LinuxForHealth/FHIR/blob/main/fhir-openapi/src/main/webapp/META-INF/openapi.json?raw=true#/components/schemas/OperationOutcome' + '404': + description: lookup Id not found + '500': + description: Internal server error. /v1/receivers/{receiverFullName}/deliveries/submitters/search: post: description: Return a list of all the providers, facilities and senders that have sent results to a receiver @@ -71,7 +94,7 @@ paths: '401': description: Unauthorized '400': - description: The search request could not be parsed + description: The search request could not be parsed '404': description: The receiver could not be found components: @@ -176,7 +199,7 @@ components: - SINCE - UNTIL value: - description: The value for the filter to apply; depending on the + description: The value for the filter to apply; depending on the > filter type the value will be parsed to the data type that underlying filter expects > i.e. the SINCE filter will parse it to an LocalDateTime type: string diff --git a/prime-router/docs/api/submissions.yml b/prime-router/docs/api/submissions.yml index 2238f2772fc..c4cae6985b5 100644 --- a/prime-router/docs/api/submissions.yml +++ b/prime-router/docs/api/submissions.yml @@ -39,7 +39,7 @@ paths: description: Return a list of all basic info on all receive actions and associated reports. security: - OAuth2: [ user ] - - ApiKeyAuth: [] + - ApiKeyAuth: [ ] parameters: - in: query name: cursor @@ -145,7 +145,7 @@ paths: get: description: Return a single receive action, and its received report(s), or an error report. security: - - ApiKeyAuth: [] + - ApiKeyAuth: [ ] parameters: - in: path name: actionid @@ -166,7 +166,29 @@ paths: description: Unauthorized '500': description: Internal Server Error - + /waters/report/{reportId}/history/etorMetadata: + get: + summary: Retrieves ETOR metadata for a sender's report + description: Fetches the ETOR intermediary's metadata given the sender's report ID. + operationId: getEtorMetadataForHistory + parameters: + - name: reportId + in: path + required: true + description: The unique identifier of the report. + schema: + type: string + responses: + '200': + description: Successful retrieval of ETOR metadata. + content: + application/json: + schema: + $ref: 'https://github.com/LinuxForHealth/FHIR/blob/main/fhir-openapi/src/main/webapp/META-INF/openapi.json?raw=true#/components/schemas/OperationOutcome' + '404': + description: lookup Id not found + '500': + description: Internal server error. # Building components: schemas: diff --git a/prime-router/docs/design/design/rs-ti-integration.md b/prime-router/docs/design/design/rs-ti-integration.md new file mode 100644 index 00000000000..c45c6b810c2 --- /dev/null +++ b/prime-router/docs/design/design/rs-ti-integration.md @@ -0,0 +1,15 @@ +# ReportStream and Intermediary Integration + +## ETOR + +Our partners on the ETOR initiative need access to ETOR-specific data +(as of April 2024: time received, a hash of the message, and any linking between orders and results, combined status across RS and TI, etc.). +The metadata response is required to be a FHIR resource, something that isn't supported by ReportStream, and that ETOR-specific functionality +belong in the ETOR microservice. The main ReportStream application needs to be the front door for authentication +and consistency purposes, while still allowing partners access to data stored in the CDC Intermediary. + +This functionality is supported by the API endpoints located in `SubmissionFunction#getEtorMetadata` and +`DeliveryFunction#getEtorMetadata`. These endpoints act as wrappers to the metadata endpoint that exists within the CDC +Intermediary. +They perform authentication against the calling entity using the existing ReportStream auth framework before +searching for the correct reportId to call the CDC Intermediary. diff --git a/prime-router/docs/docs-deprecated/getting-started/getting-started.md b/prime-router/docs/docs-deprecated/getting-started/getting-started.md index 092e1ef30c6..a26eee05800 100644 --- a/prime-router/docs/docs-deprecated/getting-started/getting-started.md +++ b/prime-router/docs/docs-deprecated/getting-started/getting-started.md @@ -305,6 +305,11 @@ When building the ReportStream container, you can set this value to `true` to en PRIME_DATA_HUB_INSECURE_SSL=true docker compose build ``` +## `ETOR_TI_baseurl` service setup + +To run the service associated with this environment variable locally, please visit the instructions located +[at the intermediary project](https://github.com/CDCgov/trusted-intermediary/blob/main/README.md) + # Troubleshooting ## Local SFTP Issues 1. SFTP Upload Permission denied - If you get a Permission Denied exception in the logs then it is most likely the atmoz/sftp diff --git a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt index c85baac95ad..0131833b381 100644 --- a/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt +++ b/prime-router/src/main/kotlin/fhirengine/engine/FHIRConverter.kt @@ -31,6 +31,7 @@ import gov.cdc.prime.router.azure.observability.event.AzureEventServiceImpl import gov.cdc.prime.router.azure.observability.event.ReportCreatedEvent import gov.cdc.prime.router.fhirengine.translation.HL7toFhirTranslator import gov.cdc.prime.router.fhirengine.translation.hl7.FhirTransformer +import gov.cdc.prime.router.fhirengine.translation.hl7.utils.FhirPathUtils import gov.cdc.prime.router.fhirengine.utils.FhirTranscoder import gov.cdc.prime.router.fhirengine.utils.HL7Reader import gov.cdc.prime.router.fhirengine.utils.HL7Reader.Companion.parseHL7Message @@ -102,6 +103,17 @@ class FHIRConverter( val format = Report.getFormatFromBlobURL(queueMessage.blobURL) logger.trace("Processing $format data for FHIR conversion.") + // This line is a workaround for a defect in the hapi-fhir library + // Specifically https://github.com/hapifhir/hapi-fhir/blob/b555498c9b7824af67b219e5b7b85f7992aec991/hapi-fhir-serviceloaders/hapi-fhir-caching-api/src/main/java/ca/uhn/fhir/sl/cache/CacheFactory.java#L32 + // which creates a static instance of ServiceLoader which the documentation indicates is not safe to use in a + // concurrent setting https://arc.net/l/quote/hauavetq. See also this closed issue https://github.com/jakartaee/jsonp-api/issues/26#issuecomment-364844610 + // for someone requesting a similar change in another library and the reasoning why it can't be done that way + // + // This line exists so that FhirPathUtils (an object) is instantiated before any of the multi-threaded code run + // (kotlin objects are instantiated at first access https://arc.net/l/quote/tbvpqnlh) + // TODO: https://github.com/CDCgov/prime-reportstream/issues/14287 + FhirPathUtils + val fhirBundles = process(format, queueMessage, actionLogger) if (fhirBundles.isNotEmpty()) { diff --git a/prime-router/src/main/kotlin/history/azure/DeliveryFunction.kt b/prime-router/src/main/kotlin/history/azure/DeliveryFunction.kt index 8dae305de67..d1b31637104 100644 --- a/prime-router/src/main/kotlin/history/azure/DeliveryFunction.kt +++ b/prime-router/src/main/kotlin/history/azure/DeliveryFunction.kt @@ -1,6 +1,7 @@ package gov.cdc.prime.router.history.azure import com.fasterxml.jackson.annotation.JsonProperty +import com.microsoft.azure.functions.ExecutionContext import com.microsoft.azure.functions.HttpMethod import com.microsoft.azure.functions.HttpRequestMessage import com.microsoft.azure.functions.HttpResponseMessage @@ -22,6 +23,7 @@ import gov.cdc.prime.router.history.db.DeliveryDatabaseAccess import gov.cdc.prime.router.history.db.ReportGraph import gov.cdc.prime.router.history.db.SubmitterApiSearch import gov.cdc.prime.router.history.db.SubmitterDatabaseAccess +import gov.cdc.prime.router.report.ReportService import gov.cdc.prime.router.tokens.AuthenticatedClaims import gov.cdc.prime.router.tokens.authenticationFailure import java.util.UUID @@ -32,10 +34,12 @@ import java.util.UUID * * @property reportFileFacade Facade class containing business logic to handle the data. * @property workflowEngine Container for helpers and accessors used when dealing with the workflow. + * @property reportService Service for querying graphs of reports or items */ class DeliveryFunction( val deliveryFacade: DeliveryFacade = DeliveryFacade.instance, workflowEngine: WorkflowEngine = WorkflowEngine(), + private val reportService: ReportService = ReportService(), ) : ReportFileFunction( deliveryFacade, workflowEngine @@ -188,6 +192,43 @@ class DeliveryFunction( return this.getDetailedView(request, id) } + /** + * Endpoint for intermediary receivers to verify status of messages. It passes + * a null engine to the retrieveMetadata function because Azure gets upset if there + * are any non-annotated parameters in the method signature other than ExecutionContext + * and we needed the engine to be a parameter so it can be mocked for tests + * + */ + @FunctionName("getEtorMetadataForDelivery") + fun getEtorMetadata( + @HttpTrigger( + name = "getEtorMetadataForDelivery", + methods = [HttpMethod.GET], + authLevel = AuthorizationLevel.ANONYMOUS, + route = "waters/report/{reportId}/delivery/etorMetadata" + ) request: HttpRequestMessage, + @BindingName("reportId") reportId: UUID, + context: ExecutionContext, + ): HttpResponseMessage { + return this.retrieveETORIntermediaryMetadata(request, reportId, context, null) + } + + /** + * Function for finding the associated report ID that the intermediary knows about given the report ID that the receiver is + * given from report stream + */ + override fun getLookupId(reportId: UUID): UUID? { + // the delivery endpoint is called by the final receiver with a sent report ID, where TI + // knows about the related submission report ID + try { + val root = reportService.getRootReport(reportId) + return root.reportId + } catch (ex: IllegalStateException) { + logger.error("Unable to locate root report for report $reportId") + return null + } + } + /** * Get a sortable list of delivery facilities * diff --git a/prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt b/prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt index 67f6efab7be..13940896937 100644 --- a/prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt +++ b/prime-router/src/main/kotlin/history/azure/ReportFileFunction.kt @@ -1,8 +1,11 @@ package gov.cdc.prime.router.history.azure +import com.microsoft.azure.functions.ExecutionContext import com.microsoft.azure.functions.HttpRequestMessage import com.microsoft.azure.functions.HttpResponseMessage +import com.microsoft.azure.functions.HttpStatus import gov.cdc.prime.router.CustomerStatus +import gov.cdc.prime.router.RESTTransportType import gov.cdc.prime.router.azure.HttpUtilities import gov.cdc.prime.router.azure.WorkflowEngine import gov.cdc.prime.router.azure.db.tables.pojos.Action @@ -10,11 +13,22 @@ import gov.cdc.prime.router.history.ReportHistory import gov.cdc.prime.router.tokens.AuthenticatedClaims import gov.cdc.prime.router.tokens.authenticationFailure import gov.cdc.prime.router.tokens.authorizationFailure +import gov.cdc.prime.router.transport.RESTTransport +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.request.get +import io.ktor.client.statement.HttpResponse +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking import org.apache.logging.log4j.kotlin.Logging import org.jooq.exception.DataAccessException import java.time.OffsetDateTime import java.time.format.DateTimeParseException import java.util.UUID +import java.util.logging.Logger /** * History API @@ -28,6 +42,8 @@ abstract class ReportFileFunction( internal val workflowEngine: WorkflowEngine = WorkflowEngine(), ) : Logging { + internal val intermediaryReceiverName = "flexion.etor-service-receiver-orders" + /** * Helper to store currently loaded action to prevent extra DB calls */ @@ -190,6 +206,87 @@ abstract class ReportFileFunction( } } + /** + * Function to return metadata of a single report from the CDC Intermediary. + * The [reportId] is a valid report UUID. This function is for the Intermediary only, please don't update + * without contacting that engineering team + */ + fun retrieveETORIntermediaryMetadata( + request: HttpRequestMessage, + reportId: UUID, + context: ExecutionContext, + engine: HttpClientEngine?, + etorTiBaseUrl: String? = System.getenv("ETOR_TI_baseurl"), + ): HttpResponseMessage { + if (etorTiBaseUrl == null) { + return HttpUtilities.httpResponse( + request, + "Environment variable ETOR_TI_baseurl is not set. Set this variable and run the TI service.", + HttpStatus.INTERNAL_SERVER_ERROR + ) + } + + val authResult = this.authSingleBlocks(request, reportId.toString()) + + if (authResult != null) { + return authResult + } + + var response: HttpResponse? + val receiver = workflowEngine.settings.findReceiver(this.intermediaryReceiverName) + val client = if (engine == null) HttpClient() else HttpClient(engine) + val restTransportInfo = receiver?.transport as RESTTransportType + val (credential, jksCredential) = RESTTransport().getCredential(restTransportInfo, receiver) + val logger: Logger = context.logger + + val authPair = runBlocking { + async { + RESTTransport().getOAuthToken( + restTransportInfo, + reportId.toString(), + jksCredential, + credential, + logger + ) + }.await() + } + + val lookupId = this.getLookupId(reportId) + ?: return HttpUtilities.notFoundResponse(request, "lookup Id not found") + + val (status, responseBody) = runBlocking { + async { + response = client.get("$etorTiBaseUrl/v1/etor/metadata/" + lookupId) { + authPair.first.forEach { entry -> + headers.append(entry.key, entry.value) + } + + headers.append(HttpHeaders.Authorization, "Bearer " + authPair.second!!) + } + + Pair(response!!.status, response!!.body()) + }.await() + } + + if (status == HttpStatusCode.NotFound) { + return HttpUtilities.notFoundResponse(request, "metadata not found") + } + + if (status.value >= 300) { + return HttpUtilities.internalErrorResponse(request) + } + + return HttpUtilities.okResponse(request, responseBody) + } + + /** + * Use the specified report ID to find the lookup ID for the ETOR TI to use to retrieve metadata + * + * @param reportId DB Action that we are reviewing + * @return the string lookup ID if found, otherwise and empty string + */ + abstract fun getLookupId(reportId: UUID): UUID? + /** * Look for an action related to the given id. * To reduce DB hits, if this object has a value set on currentAction, @@ -198,7 +295,7 @@ abstract class ReportFileFunction( * @param id Either a reportId or actionId to look for matches on. * @return The action related to the given id. */ - private fun actionFromId(id: String): Action { + fun actionFromId(id: String): Action { // Figure out whether we're dealing with an action_id or a report_id. val actionId = id.toLongOrNull() return if (currentAction != null && currentAction!!.actionId == actionId) { diff --git a/prime-router/src/main/kotlin/history/azure/SubmissionFunction.kt b/prime-router/src/main/kotlin/history/azure/SubmissionFunction.kt index 952b25fba2f..7fd7c8687ca 100644 --- a/prime-router/src/main/kotlin/history/azure/SubmissionFunction.kt +++ b/prime-router/src/main/kotlin/history/azure/SubmissionFunction.kt @@ -1,5 +1,6 @@ package gov.cdc.prime.router.history.azure +import com.microsoft.azure.functions.ExecutionContext import com.microsoft.azure.functions.HttpMethod import com.microsoft.azure.functions.HttpRequestMessage import com.microsoft.azure.functions.HttpResponseMessage @@ -11,7 +12,11 @@ import gov.cdc.prime.router.Sender import gov.cdc.prime.router.azure.WorkflowEngine import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.azure.db.tables.pojos.Action +import gov.cdc.prime.router.azure.db.tables.pojos.ReportFile import gov.cdc.prime.router.history.DetailedSubmissionHistory +import gov.cdc.prime.router.history.db.ReportGraph +import java.util.UUID +import kotlin.jvm.optionals.getOrNull /** * Submissions API @@ -126,4 +131,42 @@ class SubmissionFunction( ): HttpResponseMessage { return this.getDetailedView(request, id) } + + /** + * Endpoint for intermediary senders to verify status of messages. It passes + * a null engine to the retrieveMetadata function because Azure gets upset if there + * are any non-annotated parameters in the method signature other than ExecutionContext + * and we needed the engine to be a parameter so it can be mocked for tests + * + */ + @FunctionName("getEtorMetadataForHistory") + fun getEtorMetadata( + @HttpTrigger( + name = "getEtorMetadataForHistory", + methods = [HttpMethod.GET], + authLevel = AuthorizationLevel.ANONYMOUS, + route = "waters/report/{reportId}/history/etorMetadata" + ) request: HttpRequestMessage, + @BindingName("reportId") reportId: UUID, + context: ExecutionContext, + ): HttpResponseMessage { + return this.retrieveETORIntermediaryMetadata(request, reportId, context, null) + } + + /** + * Function for finding the associated report ID that the intermediary knows about given the report ID that the sender is + * given from report stream + */ + override fun getLookupId(reportId: UUID): UUID? { + val reportGraph = ReportGraph(workflowEngine.db) + var descendants: List = emptyList() + workflowEngine.db.transactReturning { txn -> + // looking for the descendant batch report because RS sends the batch report ID, not the send report ID, to ReST receivers + descendants = reportGraph.getDescendantReports(txn, reportId, setOf(TaskAction.batch)) + } + + return descendants.stream().filter { + it.receivingOrg == "flexion" + }.findFirst().getOrNull()?.reportId + } } \ No newline at end of file diff --git a/prime-router/src/main/kotlin/history/db/ReportGraph.kt b/prime-router/src/main/kotlin/history/db/ReportGraph.kt index 7dbc3aaa03f..7b8838f1212 100644 --- a/prime-router/src/main/kotlin/history/db/ReportGraph.kt +++ b/prime-router/src/main/kotlin/history/db/ReportGraph.kt @@ -17,6 +17,7 @@ import gov.cdc.prime.router.common.BaseEngine import org.apache.logging.log4j.kotlin.Logging import org.jooq.CommonTableExpression import org.jooq.DSLContext +import org.jooq.Record1 import org.jooq.Record2 import org.jooq.impl.CustomRecord import org.jooq.impl.CustomTable @@ -144,6 +145,24 @@ class ReportGraph( } } + /** + * Recursively goes down the report_linage table from any report until it reaches + * all descendant reports with the specified action type(s) + * + * This will return an empty list if no report with the specified action type is present or if + * the ID of the final descendant is passed in + * + * If the passed in report ID has multiple descendant reports, they will all be returned + */ + fun getDescendantReports( + txn: DataAccessTransaction, + childReportId: UUID, + searchedForTaskActions: Set, + ): List { + val cte = reportDescendantGraphCommonTableExpression(listOf(childReportId)) + return descendantReportRecords(txn, cte, searchedForTaskActions).fetchInto(ReportFile::class.java) + } + /** * Returns all the metadata rows associated with the passed in [ItemGraphRecord] * @@ -360,26 +379,44 @@ class ReportGraph( private fun reportDescendantGraphCommonTableExpression(sourceReportIds: List) = DSL.name(lineageCteName).fields( PARENT_REPORT_ID_FIELD, - PATH_FIELD ).`as`( DSL.select( - REPORT_LINEAGE.CHILD_REPORT_ID, - REPORT_LINEAGE.PARENT_REPORT_ID.cast(SQLDataType.VARCHAR), + REPORT_LINEAGE.PARENT_REPORT_ID ).from(REPORT_LINEAGE) .where(REPORT_LINEAGE.PARENT_REPORT_ID.`in`(sourceReportIds)) .unionAll( DSL.select( REPORT_LINEAGE.CHILD_REPORT_ID, - DSL.field("$lineageCteName.$PATH_FIELD", SQLDataType.VARCHAR) - .concat(REPORT_LINEAGE.CHILD_REPORT_ID) ) .from(REPORT_LINEAGE) .join(DSL.table(DSL.name(lineageCteName))) .on( DSL.field(DSL.name(lineageCteName, PARENT_REPORT_ID_FIELD), SQLDataType.UUID) - .eq(REPORT_LINEAGE.CHILD_REPORT_ID) + .eq(REPORT_LINEAGE.PARENT_REPORT_ID) ) ) ) + + /** + * Fetches all descendant report records in a recursive manner. + * + * @param txn the data access transaction + * @param cte the common table expression for report lineage + * @return the descendant report records + */ + private fun descendantReportRecords( + txn: DataAccessTransaction, + cte: CommonTableExpression>, + searchedForTaskActions: Set, + ) = DSL.using(txn) + .withRecursive(cte) + .select(REPORT_FILE.asterisk()) + .distinctOn(REPORT_FILE.REPORT_ID) + .from(cte) + .join(REPORT_FILE) + .on(REPORT_FILE.REPORT_ID.eq(cte.field(0, UUID::class.java))) + .join(ACTION) + .on(ACTION.ACTION_ID.eq(REPORT_FILE.ACTION_ID)) + .where(ACTION.ACTION_NAME.`in`(searchedForTaskActions)) } \ No newline at end of file diff --git a/prime-router/src/test/kotlin/history/azure/DeliveryFunctionTests.kt b/prime-router/src/test/kotlin/history/azure/DeliveryFunctionTests.kt index 39f93f45e4f..76625ba487d 100644 --- a/prime-router/src/test/kotlin/history/azure/DeliveryFunctionTests.kt +++ b/prime-router/src/test/kotlin/history/azure/DeliveryFunctionTests.kt @@ -1,20 +1,24 @@ package gov.cdc.prime.router.history.azure import assertk.assertThat +import assertk.assertions.contains import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import com.fasterxml.jackson.module.kotlin.readValue import com.google.common.net.HttpHeaders +import com.microsoft.azure.functions.ExecutionContext import com.microsoft.azure.functions.HttpStatus import gov.cdc.prime.router.CovidSender import gov.cdc.prime.router.CustomerStatus import gov.cdc.prime.router.Metadata import gov.cdc.prime.router.Organization +import gov.cdc.prime.router.RESTTransportType import gov.cdc.prime.router.Receiver import gov.cdc.prime.router.Schema import gov.cdc.prime.router.Sender import gov.cdc.prime.router.SettingsProvider import gov.cdc.prime.router.Topic +import gov.cdc.prime.router.TranslatorConfiguration import gov.cdc.prime.router.azure.ApiSearchResult import gov.cdc.prime.router.azure.DatabaseAccess import gov.cdc.prime.router.azure.MockHttpRequestMessage @@ -26,6 +30,8 @@ import gov.cdc.prime.router.azure.db.tables.pojos.CovidResultMetadata import gov.cdc.prime.router.azure.db.tables.pojos.ReportFile import gov.cdc.prime.router.common.BaseEngine import gov.cdc.prime.router.common.JacksonMapperUtilities +import gov.cdc.prime.router.credentials.RestCredential +import gov.cdc.prime.router.credentials.UserJksCredential import gov.cdc.prime.router.history.DeliveryFacility import gov.cdc.prime.router.history.DeliveryHistory import gov.cdc.prime.router.history.db.Delivery @@ -34,13 +40,22 @@ import gov.cdc.prime.router.history.db.ReportGraph import gov.cdc.prime.router.history.db.Submitter import gov.cdc.prime.router.history.db.SubmitterDatabaseAccess import gov.cdc.prime.router.history.db.SubmitterType +import gov.cdc.prime.router.report.ReportService import gov.cdc.prime.router.tokens.AuthenticatedClaims import gov.cdc.prime.router.tokens.AuthenticationType import gov.cdc.prime.router.tokens.OktaAuthentication import gov.cdc.prime.router.tokens.TestDefaultJwt import gov.cdc.prime.router.tokens.oktaSystemAdminGroup +import gov.cdc.prime.router.transport.RESTTransport import gov.cdc.prime.router.unittest.UnitTestUtils +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf import io.mockk.clearAllMocks +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.mockkClass @@ -60,6 +75,7 @@ import org.junit.jupiter.api.TestInstance import java.time.Instant import java.time.OffsetDateTime import java.util.UUID +import java.util.logging.Logger import kotlin.test.Test @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -322,6 +338,7 @@ class DeliveryFunctionTests : Logging { private fun setupDeliveryFunctionForTesting( oktaClaimsOrganizationName: String, facade: DeliveryFacade, + reportService: ReportService = ReportService(), ): DeliveryFunction { val claimsMap = buildClaimsMap(oktaClaimsOrganizationName) val metadata = Metadata(schema = Schema(name = "one", topic = Topic.TEST)) @@ -361,6 +378,24 @@ class DeliveryFunctionTests : Logging { ) settings.receiverStore[receiver2.fullName] = receiver2 + val receiver3 = Receiver( + "flexion.etor-service-receiver-orders", + "flexion.etor-service-receiver-orders", + Topic.ETOR_TI, + CustomerStatus.ACTIVE, + mockk(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + false, + emptyList(), + emptyList(), + false, + "test", null, "", mockk(), "", emptyList() + ) + settings.receiverStore["flexion.etor-service-receiver-orders"] = receiver3 + val engine = makeEngine(metadata, settings) mockkObject(OktaAuthentication.Companion) every { OktaAuthentication.Companion.decodeJwt(any()) } returns @@ -373,7 +408,8 @@ class DeliveryFunctionTests : Logging { return DeliveryFunction( deliveryFacade = facade, - workflowEngine = engine + workflowEngine = engine, + reportService = reportService, ) } @@ -636,6 +672,177 @@ class DeliveryFunctionTests : Logging { assertThat(response.status).isEqualTo(HttpStatus.NOT_FOUND) } + @Test + fun `test getEtorMetadata`() { + val goodUuid = UUID.fromString("662202ba-e3e5-4810-8cb8-161b75c63bc1") + val mockRequest = MockHttpRequestMessage() + mockRequest.httpHeaders[HttpHeaders.AUTHORIZATION.lowercase()] = "Bearer dummy" + val mockDeliveryFacade = mockk() + val mockReportService = mockk() + val function = setupDeliveryFunctionForTesting(oktaSystemAdminGroup, mockDeliveryFacade, mockReportService) + mockkObject(AuthenticatedClaims.Companion) + every { AuthenticatedClaims.authenticate(any()) } returns + AuthenticatedClaims.generateTestClaims() + + // Good return + val returnBody = DeliveryHistory( + 550, OffsetDateTime.now(), "test", goodUuid.toString(), Topic.ETOR_TI, + 1, "flexion", "flexion", "", "test-schema", + "body", "test" + ) + + returnBody.originalIngestion = listOf( + mapOf( + "ingestionTime" to OffsetDateTime.now(), "reportId" to goodUuid + ) + ).toMutableList() + + mockkConstructor(RESTTransport::class) + mockkConstructor(HttpClient::class) + val action = Action() + action.actionId = 550 + action.sendingOrg = organizationName + action.actionName = TaskAction.send + + val firstReport = ReportFile() + firstReport.reportId = UUID.randomUUID() + firstReport.createdAt = OffsetDateTime.parse("2023-04-18T23:36:00Z") + + every { mockReportService.getRootReport(any()) } returns firstReport + + every { mockDeliveryFacade.fetchActionForReportId(any()) } returns action + every { mockDeliveryFacade.fetchAction(any()) } returns null // not used for a UUID + every { mockDeliveryFacade.findDetailedDeliveryHistory(any()) } returns returnBody + every { mockDeliveryFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true + + val restCreds = mockk() + val userCreds = mockk() + + val creds = Pair(restCreds, userCreds) + + every { anyConstructed().getCredential(any(), any()) } returns creds + + coEvery { + anyConstructed().getOAuthToken( + any(), + any(), + any(), + any(), + any() + ) + } returns Pair(mapOf("a" to "b"), "TEST") + + val mock = MockEngine { + respond( + "{}", + HttpStatusCode.OK, + headersOf("Content-Type", ContentType.Application.Json.toString()) + ) + } + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, goodUuid, customContext, mock, "etor_base_url" + ) + + assertThat(response.status).isEqualTo(HttpStatus.OK) + } + + @Test + fun `test getEtorMetadata returns 401 when not authorized`() { + val badUuid = "762202ba-e3e5-4810-8cb8-161b75c63bc1" + val mockRequest = MockHttpRequestMessage() + mockRequest.httpHeaders[HttpHeaders.AUTHORIZATION.lowercase()] = "Bearer dummy" + val mockDeliveryFacade = mockk() + val function = setupDeliveryFunctionForTesting(oktaSystemAdminGroup, mockDeliveryFacade) + mockkObject(AuthenticatedClaims.Companion) + every { AuthenticatedClaims.authenticate(any()) } returns + null + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, UUID.fromString(badUuid), customContext, null, "etor_base_url" + ) + + assertThat(response.status).isEqualTo(HttpStatus.UNAUTHORIZED) + assertThat(response.body.toString()).isEqualTo("{\"error\": \"Authentication Failed\"}") + } + + @Test + fun `test getEtorMetadata returns 500 when the ETOR TI base URL is not set`() { + val mockRequest = MockHttpRequestMessage() + val mockDeliveryFacade = mockk() + val function = setupDeliveryFunctionForTesting(oktaSystemAdminGroup, mockDeliveryFacade) + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, UUID.randomUUID(), customContext, null, null + ) + + assertThat(response.status).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + assertThat(response.body.toString()).contains("ETOR_TI_baseurl") + } + + @Test + fun `test getEtorMetadata returns 404 when ID is invalid`() { + val badUuid = "762202ba-e3e5-4810-8cb8-161b75c63bc1" + val mockRequest = MockHttpRequestMessage() + mockRequest.httpHeaders[HttpHeaders.AUTHORIZATION.lowercase()] = "Bearer dummy" + val mockDeliveryFacade = mockk() + val mockReportService = mockk() + val function = setupDeliveryFunctionForTesting(oktaSystemAdminGroup, mockDeliveryFacade, mockReportService) + mockkObject(AuthenticatedClaims.Companion) + every { AuthenticatedClaims.authenticate(any()) } returns + AuthenticatedClaims.generateTestClaims() + + mockkConstructor(RESTTransport::class) + mockkConstructor(HttpClient::class) + val action = Action() + action.actionId = 550 + action.sendingOrg = organizationName + action.actionName = TaskAction.send + + every { mockReportService.getRootReport(any()) } throws IllegalStateException("can't find the root report") + + every { mockDeliveryFacade.fetchActionForReportId(any()) } returns action + every { mockDeliveryFacade.fetchAction(any()) } returns null // not used for a UUID + every { mockDeliveryFacade.findDetailedDeliveryHistory(any()) } returns null + every { mockDeliveryFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true + + val restCreds = mockk() + val userCreds = mockk() + + val creds = Pair(restCreds, userCreds) + + every { anyConstructed().getCredential(any(), any()) } returns creds + + coEvery { + anyConstructed().getOAuthToken( + any(), + any(), + any(), + any(), + any() + ) + } returns Pair(mapOf("a" to "b"), "TEST") + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, UUID.fromString(badUuid), customContext, null, "etor_base_url" + ) + + assertThat(response.status).isEqualTo(HttpStatus.NOT_FOUND) + assertThat(response.body.toString()).isEqualTo("{\"error\": \"lookup Id not found\"}") + } + @Nested inner class TestGetSubmitters() { var settings = MockSettings() diff --git a/prime-router/src/test/kotlin/history/azure/SubmissionFunctionTests.kt b/prime-router/src/test/kotlin/history/azure/SubmissionFunctionTests.kt index f9c490c5b74..23b73c483ee 100644 --- a/prime-router/src/test/kotlin/history/azure/SubmissionFunctionTests.kt +++ b/prime-router/src/test/kotlin/history/azure/SubmissionFunctionTests.kt @@ -1,37 +1,57 @@ package gov.cdc.prime.router.history.azure import assertk.assertThat +import assertk.assertions.contains import assertk.assertions.isEqualTo import com.fasterxml.jackson.module.kotlin.readValue import com.google.common.net.HttpHeaders +import com.microsoft.azure.functions.ExecutionContext import com.microsoft.azure.functions.HttpStatus import gov.cdc.prime.router.ClientSource import gov.cdc.prime.router.CovidSender import gov.cdc.prime.router.CustomerStatus import gov.cdc.prime.router.Metadata +import gov.cdc.prime.router.RESTTransportType +import gov.cdc.prime.router.Receiver import gov.cdc.prime.router.ReportId import gov.cdc.prime.router.Schema import gov.cdc.prime.router.Sender import gov.cdc.prime.router.SettingsProvider import gov.cdc.prime.router.Topic +import gov.cdc.prime.router.TranslatorConfiguration import gov.cdc.prime.router.azure.DatabaseAccess import gov.cdc.prime.router.azure.MockHttpRequestMessage import gov.cdc.prime.router.azure.MockSettings import gov.cdc.prime.router.azure.WorkflowEngine import gov.cdc.prime.router.azure.db.enums.TaskAction import gov.cdc.prime.router.azure.db.tables.pojos.Action +import gov.cdc.prime.router.azure.db.tables.pojos.ReportFile import gov.cdc.prime.router.cli.tests.ExpectedSubmissionList import gov.cdc.prime.router.common.JacksonMapperUtilities +import gov.cdc.prime.router.credentials.RestCredential +import gov.cdc.prime.router.credentials.UserJksCredential +import gov.cdc.prime.router.history.Destination +import gov.cdc.prime.router.history.DetailedReport import gov.cdc.prime.router.history.DetailedSubmissionHistory import gov.cdc.prime.router.history.SubmissionHistory +import gov.cdc.prime.router.history.db.ReportGraph import gov.cdc.prime.router.tokens.AuthenticatedClaims import gov.cdc.prime.router.tokens.OktaAuthentication import gov.cdc.prime.router.tokens.TestDefaultJwt import gov.cdc.prime.router.tokens.oktaSystemAdminGroup +import gov.cdc.prime.router.transport.RESTTransport +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.http.headersOf import io.mockk.clearAllMocks +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.mockkClass +import io.mockk.mockkConstructor import io.mockk.mockkObject import io.mockk.spyk import org.apache.logging.log4j.kotlin.Logging @@ -43,6 +63,8 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.TestInstance import java.time.Instant import java.time.OffsetDateTime +import java.util.UUID +import java.util.logging.Logger import kotlin.test.Test @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -312,8 +334,28 @@ class SubmissionFunctionTests : Logging { customerStatus = CustomerStatus.INACTIVE, schemaName = "one" ) + + val receiver = Receiver( + "flexion.etor-service-receiver-orders", + "flexion.etor-service-receiver-orders", + Topic.ETOR_TI, + CustomerStatus.ACTIVE, + mockk(), + emptyList(), + emptyList(), + emptyList(), + emptyList(), + false, + emptyList(), + emptyList(), + false, + "test", null, "", mockk(), "", emptyList() + ) + settings.senderStore[sender2.fullName] = sender2 + settings.receiverStore["flexion.etor-service-receiver-orders"] = receiver val engine = makeEngine(metadata, settings) + engine.settings.receivers.plus(receiver) mockkObject(OktaAuthentication.Companion) every { OktaAuthentication.Companion.decodeJwt(any()) } returns TestDefaultJwt( @@ -336,13 +378,13 @@ class SubmissionFunctionTests : Logging { return httpRequestMessage } - @Test - fun `test access user can view their organization's submission history`() { - val submissionFunction = setupSubmissionFunctionForTesting(oktaClaimsOrganizationName, mockFacade()) - val httpRequestMessage = setupHttpRequestMessageForTesting() - val response = submissionFunction.getOrgSubmissionsList(httpRequestMessage, organizationName) - assertThat(response.status).isEqualTo(HttpStatus.OK) - } + @Test + fun `test access user can view their organization's submission history`() { + val submissionFunction = setupSubmissionFunctionForTesting(oktaClaimsOrganizationName, mockFacade()) + val httpRequestMessage = setupHttpRequestMessageForTesting() + val response = submissionFunction.getOrgSubmissionsList(httpRequestMessage, organizationName) + assertThat(response.status).isEqualTo(HttpStatus.OK) + } @Test fun `test access user cannot view another organization's submission history`() { @@ -471,4 +513,367 @@ class SubmissionFunctionTests : Logging { response = function.getReportDetailedHistory(mockRequest, emptyActionId) assertThat(response.status).isEqualTo(HttpStatus.NOT_FOUND) } - } \ No newline at end of file + + @Test + fun `test getEtorMetadata`() { + val goodUuid = "662202ba-e3e5-4810-8cb8-161b75c63bc1" + val mockRequest = MockHttpRequestMessage() + mockRequest.httpHeaders[HttpHeaders.AUTHORIZATION.lowercase()] = "Bearer dummy" + val mockSubmissionFacade = mockk() + val function = setupSubmissionFunctionForTesting(oktaSystemAdminGroup, mockSubmissionFacade) + mockkObject(AuthenticatedClaims.Companion) + every { AuthenticatedClaims.authenticate(any()) } returns + AuthenticatedClaims.generateTestClaims() + + val detailedReport = DetailedReport( + UUID.randomUUID(), + "flexion", "flexion", "lab", "lab", Topic.ETOR_TI, "external", + null, null, 1, 1, true + ) + + // Good return + val returnBody = DetailedSubmissionHistory( + 550, TaskAction.receive, OffsetDateTime.now(), 201, + mutableListOf(detailedReport) + ) + + returnBody.destinations = listOf( + Destination( + "flexion", "test", mutableListOf(), mutableListOf(), + OffsetDateTime.now(), 1, 1, mutableListOf(detailedReport), mutableListOf() + ) + ).toMutableList() + + mockkConstructor(RESTTransport::class) + mockkConstructor(HttpClient::class) + val action = Action() + action.actionId = 550 + action.sendingOrg = organizationName + action.actionName = TaskAction.receive + + mockkConstructor(ReportGraph::class) + + val firstReport = ReportFile() + firstReport.reportId = UUID.randomUUID() + firstReport.receivingOrg = "not-flexion" + + val secondReport = ReportFile() + secondReport.reportId = UUID.randomUUID() + secondReport.receivingOrg = "flexion" + + every { + anyConstructed().getDescendantReports(any(), any(), any()) + } returns listOf(firstReport, secondReport) + every { mockSubmissionFacade.fetchActionForReportId(any()) } returns action + every { mockSubmissionFacade.fetchAction(any()) } returns null // not used for a UUID + every { mockSubmissionFacade.findDetailedSubmissionHistory(any()) } returns returnBody + every { mockSubmissionFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true + + val restCreds = mockk() + val userCreds = mockk() + + val creds = Pair(restCreds, userCreds) + + every { anyConstructed().getCredential(any(), any()) } returns creds + + coEvery { + anyConstructed().getOAuthToken( + any(), + any(), + any(), + any(), + any() + ) + } returns Pair(mapOf("a" to "b"), "TEST") + + val mock = MockEngine { + respond( + "{}", + HttpStatusCode.OK, + headersOf("Content-Type", ContentType.Application.Json.toString()) + ) + } + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, UUID.fromString(goodUuid), customContext, mock, "etor_base_url" + ) + + assertThat(response.status).isEqualTo(HttpStatus.OK) + } + + @Test + fun `test getEtorMetadata returns 404 when ID is invalid`() { + val badUuid = "762202ba-e3e5-4810-8cb8-161b75c63bc1" + val mockRequest = MockHttpRequestMessage() + mockRequest.httpHeaders[HttpHeaders.AUTHORIZATION.lowercase()] = "Bearer dummy" + val mockSubmissionFacade = mockk() + val function = setupSubmissionFunctionForTesting(oktaSystemAdminGroup, mockSubmissionFacade) + mockkObject(AuthenticatedClaims.Companion) + every { AuthenticatedClaims.authenticate(any()) } returns + AuthenticatedClaims.generateTestClaims() + + mockkConstructor(RESTTransport::class) + mockkConstructor(HttpClient::class) + val action = Action() + action.actionId = 550 + action.sendingOrg = organizationName + action.actionName = TaskAction.receive + + mockkConstructor(ReportGraph::class) + + every { anyConstructed().getDescendantReports(any(), any(), any()) } returns emptyList() + every { mockSubmissionFacade.fetchActionForReportId(any()) } returns action + every { mockSubmissionFacade.fetchAction(any()) } returns null // not used for a UUID + every { mockSubmissionFacade.findDetailedSubmissionHistory(any()) } returns null + every { mockSubmissionFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true + + val restCreds = mockk() + val userCreds = mockk() + + val creds = Pair(restCreds, userCreds) + + every { anyConstructed().getCredential(any(), any()) } returns creds + + coEvery { + anyConstructed().getOAuthToken( + any(), + any(), + any(), + any(), + any() + ) + } returns Pair(mapOf("a" to "b"), "TEST") + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, UUID.fromString(badUuid), customContext, null, "etor_base_url" + ) + + assertThat(response.status).isEqualTo(HttpStatus.NOT_FOUND) + assertThat(response.body.toString()).isEqualTo("{\"error\": \"lookup Id not found\"}") + } + + @Test + fun `test getEtorMetadata returns 404 when TI returns 404`() { + val goodUuid = "662202ba-e3e5-4810-8cb8-161b75c63bc1" + val mockRequest = MockHttpRequestMessage() + mockRequest.httpHeaders[HttpHeaders.AUTHORIZATION.lowercase()] = "Bearer dummy" + val mockSubmissionFacade = mockk() + val function = setupSubmissionFunctionForTesting(oktaSystemAdminGroup, mockSubmissionFacade) + mockkObject(AuthenticatedClaims.Companion) + every { AuthenticatedClaims.authenticate(any()) } returns + AuthenticatedClaims.generateTestClaims() + + val detailedReport = DetailedReport( + UUID.randomUUID(), + "flexion", "flexion", "lab", "lab", Topic.ETOR_TI, "external", + null, null, 1, 1, true + ) + + // Good return + val returnBody = DetailedSubmissionHistory( + 550, TaskAction.receive, OffsetDateTime.now(), 201, + mutableListOf(detailedReport) + ) + + returnBody.destinations = listOf( + Destination( + "flexion", "test", mutableListOf(), mutableListOf(), + OffsetDateTime.now(), 1, 1, mutableListOf(detailedReport), mutableListOf() + ) + ).toMutableList() + + mockkConstructor(RESTTransport::class) + mockkConstructor(HttpClient::class) + val action = Action() + action.actionId = 550 + action.sendingOrg = organizationName + action.actionName = TaskAction.receive + + mockkConstructor(ReportGraph::class) + + val firstReport = ReportFile() + firstReport.reportId = UUID.randomUUID() + firstReport.receivingOrg = "not-flexion" + + val secondReport = ReportFile() + secondReport.reportId = UUID.randomUUID() + secondReport.receivingOrg = "flexion" + + every { + anyConstructed().getDescendantReports(any(), any(), any()) + } returns listOf(firstReport, secondReport) + every { mockSubmissionFacade.fetchActionForReportId(any()) } returns action + every { mockSubmissionFacade.fetchAction(any()) } returns null // not used for a UUID + every { mockSubmissionFacade.findDetailedSubmissionHistory(any()) } returns returnBody + every { mockSubmissionFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true + + val restCreds = mockk() + val userCreds = mockk() + + val creds = Pair(restCreds, userCreds) + + every { anyConstructed().getCredential(any(), any()) } returns creds + + coEvery { + anyConstructed().getOAuthToken( + any(), + any(), + any(), + any(), + any() + ) + } returns Pair(mapOf("a" to "b"), "TEST") + + val mock = MockEngine { + respond( + "{}", + HttpStatusCode.NotFound, + headersOf("Content-Type", ContentType.Application.Json.toString()) + ) + } + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, UUID.fromString(goodUuid), customContext, mock, "etor_base_url" + ) + + assertThat(response.status).isEqualTo(HttpStatus.NOT_FOUND) + } + + @Test + fun `test getEtorMetadata returns 500 for non-404 errors`() { + val goodUuid = "662202ba-e3e5-4810-8cb8-161b75c63bc1" + val mockRequest = MockHttpRequestMessage() + mockRequest.httpHeaders[HttpHeaders.AUTHORIZATION.lowercase()] = "Bearer dummy" + val mockSubmissionFacade = mockk() + val function = setupSubmissionFunctionForTesting(oktaSystemAdminGroup, mockSubmissionFacade) + mockkObject(AuthenticatedClaims.Companion) + every { AuthenticatedClaims.authenticate(any()) } returns + AuthenticatedClaims.generateTestClaims() + + val detailedReport = DetailedReport( + UUID.randomUUID(), + "flexion", "flexion", "lab", "lab", Topic.ETOR_TI, "external", + null, null, 1, 1, true + ) + + // Good return + val returnBody = DetailedSubmissionHistory( + 550, TaskAction.receive, OffsetDateTime.now(), 201, + mutableListOf(detailedReport) + ) + + returnBody.destinations = listOf( + Destination( + "flexion", "test", mutableListOf(), mutableListOf(), + OffsetDateTime.now(), 1, 1, mutableListOf(detailedReport), mutableListOf() + ) + ).toMutableList() + + mockkConstructor(RESTTransport::class) + mockkConstructor(HttpClient::class) + val action = Action() + action.actionId = 550 + action.sendingOrg = organizationName + action.actionName = TaskAction.receive + + mockkConstructor(ReportGraph::class) + + val firstReport = ReportFile() + firstReport.reportId = UUID.randomUUID() + firstReport.receivingOrg = "not-flexion" + + val secondReport = ReportFile() + secondReport.reportId = UUID.randomUUID() + secondReport.receivingOrg = "flexion" + + every { + anyConstructed().getDescendantReports(any(), any(), any()) + } returns listOf(firstReport, secondReport) + every { mockSubmissionFacade.fetchActionForReportId(any()) } returns action + every { mockSubmissionFacade.fetchAction(any()) } returns null // not used for a UUID + every { mockSubmissionFacade.findDetailedSubmissionHistory(any()) } returns returnBody + every { mockSubmissionFacade.checkAccessAuthorizationForAction(any(), any(), any()) } returns true + + val restCreds = mockk() + val userCreds = mockk() + + val creds = Pair(restCreds, userCreds) + + every { anyConstructed().getCredential(any(), any()) } returns creds + + coEvery { + anyConstructed().getOAuthToken( + any(), + any(), + any(), + any(), + any() + ) + } returns Pair(mapOf("a" to "b"), "TEST") + + val mock = MockEngine { + respond( + "{}", + HttpStatusCode.Forbidden, + headersOf("Content-Type", ContentType.Application.Json.toString()) + ) + } + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, UUID.fromString(goodUuid), customContext, mock, "etor_base_url" + ) + + assertThat(response.status).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + } + + @Test + fun `test getEtorMetadata returns 401 when not authorized`() { + val badUuid = "762202ba-e3e5-4810-8cb8-161b75c63bc1" + val mockRequest = MockHttpRequestMessage() + mockRequest.httpHeaders[HttpHeaders.AUTHORIZATION.lowercase()] = "Bearer dummy" + val mockSubmissionFacade = mockk() + val function = setupSubmissionFunctionForTesting(oktaSystemAdminGroup, mockSubmissionFacade) + mockkObject(AuthenticatedClaims.Companion) + every { AuthenticatedClaims.authenticate(any()) } returns + null + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, UUID.fromString(badUuid), customContext, null, "etor_base_url" + ) + + assertThat(response.status).isEqualTo(HttpStatus.UNAUTHORIZED) + assertThat(response.body.toString()).isEqualTo("{\"error\": \"Authentication Failed\"}") + } + + @Test + fun `test getEtorMetadata returns 500 when the ETOR TI base URL is not set`() { + val mockRequest = MockHttpRequestMessage() + val mockSubmissionFacade = mockk() + val function = setupSubmissionFunctionForTesting(oktaSystemAdminGroup, mockSubmissionFacade) + + val customContext = mockk() + every { customContext.logger } returns mockk() + + val response = function.retrieveETORIntermediaryMetadata( + mockRequest, UUID.randomUUID(), customContext, null, null + ) + + assertThat(response.status).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + assertThat(response.body.toString()).contains("ETOR_TI_baseurl") + } +} \ No newline at end of file diff --git a/prime-router/src/test/kotlin/history/db/ReportGraphTest.kt b/prime-router/src/test/kotlin/history/db/ReportGraphTest.kt index 179d9604c4d..60a4e6ccf8d 100644 --- a/prime-router/src/test/kotlin/history/db/ReportGraphTest.kt +++ b/prime-router/src/test/kotlin/history/db/ReportGraphTest.kt @@ -143,7 +143,7 @@ class ReportGraphTest { .setExternalName("batch-name") .setBodyUrl("batch-url") - val sendAction = Action().setActionId(6) + val sendAction = Action().setActionId(6).setActionName(TaskAction.send) val sendReportId = UUID.randomUUID() val sendReportFile = ReportFile() .setSchemaTopic(Topic.ELR_ELIMS) @@ -211,7 +211,7 @@ class ReportGraphTest { .insertReportLineage( ReportLineage( 0, - receiveAction.actionId, + convertAction.actionId, receivedReportFile.reportId, convertReportFile.reportId, OffsetDateTime.now() @@ -222,7 +222,7 @@ class ReportGraphTest { .insertReportLineage( ReportLineage( 1, - convertAction.actionId, + routeAction.actionId, convertReportFile.reportId, routeReportFile.reportId, OffsetDateTime.now() @@ -233,7 +233,7 @@ class ReportGraphTest { .insertReportLineage( ReportLineage( 2, - routeAction.actionId, + translateAction.actionId, routeReportFile.reportId, translateReportFile.reportId, OffsetDateTime.now() @@ -245,7 +245,7 @@ class ReportGraphTest { .insertReportLineage( ReportLineage( 3, - translateAction.actionId, + batchAction.actionId, translateReportFile.reportId, batchReportFile.reportId, OffsetDateTime.now() @@ -257,7 +257,7 @@ class ReportGraphTest { .insertReportLineage( ReportLineage( 5, - receiveAction2.actionId, + convertAction2.actionId, receivedReportFile2.reportId, convertReportFile2.reportId, OffsetDateTime.now() @@ -268,7 +268,7 @@ class ReportGraphTest { .insertReportLineage( ReportLineage( 6, - convertAction2.actionId, + routeAction2.actionId, convertReportFile2.reportId, routeReportFile2.reportId, OffsetDateTime.now() @@ -279,7 +279,7 @@ class ReportGraphTest { .insertReportLineage( ReportLineage( 7, - routeAction2.actionId, + translateAction2.actionId, routeReportFile2.reportId, translateReportFile2.reportId, OffsetDateTime.now() @@ -291,7 +291,7 @@ class ReportGraphTest { .insertReportLineage( ReportLineage( 8, - translateAction2.actionId, + batchAction.actionId, translateReportFile2.reportId, batchReportFile.reportId, OffsetDateTime.now() @@ -303,7 +303,7 @@ class ReportGraphTest { .insertReportLineage( ReportLineage( 4, - batchAction.actionId, + sendAction.actionId, batchReportFile.reportId, sendReportFile.reportId, OffsetDateTime.now() @@ -354,5 +354,19 @@ class ReportGraphTest { assertThat(roots[0].reportId).isEqualTo(receivedReportId) assertThat(roots[1].reportId).isEqualTo(receivedReportId2) } + + @Test + fun `find descendant reports from receive parent report`() { + var descendants: List = emptyList() + + ReportStreamTestDatabaseContainer.testDatabaseAccess.transact { txn -> + descendants = reportGraph.getDescendantReports(txn, receivedReportId, setOf(TaskAction.send)) + } + + assertThat(descendants) + .isNotNull() + .hasSize(1) + assertThat(descendants[0].reportId).isEqualTo(sendReportId) + } } } \ No newline at end of file From 1e1dd6e1266cf03d2d92325e86777c4393b32b07 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:08:21 -0400 Subject: [PATCH 2/5] Deployment of 2024-07-23 (#15303) * Initial commit MS receiver UP migration * Update element's names in receiver transform * Rename the setting file * Remove order from receiver transform * Relocate the setting * Updated the PR * Renamed files: - metadata/fhir_transforms/senders/Flexion/TILabOrder.yml => ti-sender-transform.yml - metadata/hl7_mapping/receivers/Flexion/TILabOrder.yml => ti-oml-receiver-transform.yml * NM migration pre work (#15005) * NM migration pre work * Added dummy filters to avoid routing all ETOR ORUs to these receivers * Updated receiver name * Update names to replace 'ti' with 'etor' for clarity * Version statement deprecated (#14702) * source sftp image from local registry * Integrate new routing functions (#14904) * Integrate new routing functions; split + refactor routing integration tests * Reworked some common helper methods * Implemented LocalAzureEventServiceImpl for testing * Added AzureEvent assertions; more test cleanup * receiver-filter edition: Added AzureEvent assertions; more test cleanup * Fix legacy test cases * Return root report if its uuid is provided * Remove unnecessary lineage from int. tests * Adjust legacy test for updated rootReport logic * Adjust docs to reflect new route steps * Implement new tests for condition filter edge cases * Lots of cleanup * Complete Events sections; address feedback; rework logging section * Address feedback * Adjust submission history tests * Adjust submission integration tests * Test adjustments * Use default sftp * Engagement/jessica/14871 Vermont migration pre work (#15170) * Fixing VT enrichments * Bump gradle/actions from 3.4.2 to 3.5.0 (#15199) Bumps [gradle/actions](https://github.com/gradle/actions) from 3.4.2 to 3.5.0. - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/dbbdc275be76ac10734476cc723d82dfe7ec6eda...d9c87d481d55275bb5441eef3fe0e46805f9ef70) --- updated-dependencies: - dependency-name: gradle/actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump gradle/actions in /.github/actions/build-backend (#15198) Bumps [gradle/actions](https://github.com/gradle/actions) from 3.4.2 to 3.5.0. - [Release notes](https://github.com/gradle/actions/releases) - [Commits](https://github.com/gradle/actions/compare/dbbdc275be76ac10734476cc723d82dfe7ec6eda...d9c87d481d55275bb5441eef3fe0e46805f9ef70) --- updated-dependencies: - dependency-name: gradle/actions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update setting * Fixed the setting * IL UP Receiver migration Pre-Work (#14555) * IL UP Receiver migration Pre-Work * --- updated-dependencies: - dependency-name: dompurify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * getting all public routes via xml * link validity checker * remove manual link checks * PR comments * Bump @types/lodash from 4.17.1 to 4.17.4 in /frontend-react Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.1 to 4.17.4. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) --- updated-dependencies: - dependency-name: "@types/lodash" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump @okta/okta-signin-widget in /frontend-react in the okta group Bumps the okta group in /frontend-react with 1 update: [@okta/okta-signin-widget](https://github.com/okta/okta-signin-widget). Updates `@okta/okta-signin-widget` from 7.18.0 to 7.18.1 - [Release notes](https://github.com/okta/okta-signin-widget/releases) - [Changelog](https://github.com/okta/okta-signin-widget/blob/master/webpack.release.config.js) - [Commits](https://github.com/okta/okta-signin-widget/compare/okta-signin-widget-7.18.0...okta-signin-widget-7.18.1) --- updated-dependencies: - dependency-name: "@okta/okta-signin-widget" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: okta ... Signed-off-by: dependabot[bot] * Bump otpauth from 9.2.3 to 9.2.4 in /frontend-react Bumps [otpauth](https://github.com/hectorm/otpauth) from 9.2.3 to 9.2.4. - [Release notes](https://github.com/hectorm/otpauth/releases) - [Commits](https://github.com/hectorm/otpauth/compare/v9.2.3...v9.2.4) --- updated-dependencies: - dependency-name: otpauth dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump the applicationinsights group in /frontend-react with 2 updates Bumps the applicationinsights group in /frontend-react with 2 updates: [@microsoft/applicationinsights-react-js](https://github.com/microsoft/applicationinsights-react-js) and [@microsoft/applicationinsights-web](https://github.com/microsoft/ApplicationInsights-JS). Updates `@microsoft/applicationinsights-react-js` from 17.1.2 to 17.2.0 - [Release notes](https://github.com/microsoft/applicationinsights-react-js/releases) - [Changelog](https://github.com/microsoft/applicationinsights-react-js/blob/main/RELEASES.md) - [Commits](https://github.com/microsoft/applicationinsights-react-js/compare/17.1.2...17.2.0) Updates `@microsoft/applicationinsights-web` from 3.2.0 to 3.2.1 - [Release notes](https://github.com/microsoft/ApplicationInsights-JS/releases) - [Changelog](https://github.com/microsoft/ApplicationInsights-JS/blob/main/RELEASES.md) - [Commits](https://github.com/microsoft/ApplicationInsights-JS/commits) --- updated-dependencies: - dependency-name: "@microsoft/applicationinsights-react-js" dependency-type: direct:production update-type: version-update:semver-minor dependency-group: applicationinsights - dependency-name: "@microsoft/applicationinsights-web" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: applicationinsights ... Signed-off-by: dependabot[bot] * Bump react-helmet-async from 2.0.4 to 2.0.5 in /frontend-react Bumps [react-helmet-async](https://github.com/staylor/react-helmet-async) from 2.0.4 to 2.0.5. - [Release notes](https://github.com/staylor/react-helmet-async/releases) - [Commits](https://github.com/staylor/react-helmet-async/commits) --- updated-dependencies: - dependency-name: react-helmet-async dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump the react-router group in /frontend-react with 2 updates Bumps the react-router group in /frontend-react with 2 updates: [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) and [react-router-dom](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router-dom). Updates `react-router` from 6.23.0 to 6.23.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@6.23.1/packages/react-router) Updates `react-router-dom` from 6.23.0 to 6.23.1 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router-dom@6.23.1/packages/react-router-dom) --- updated-dependencies: - dependency-name: react-router dependency-type: direct:production update-type: version-update:semver-patch dependency-group: react-router - dependency-name: react-router-dom dependency-type: direct:production update-type: version-update:semver-patch dependency-group: react-router ... Signed-off-by: dependabot[bot] * Bump msw from 2.2.14 to 2.3.0 in /frontend-react in the msw group Bumps the msw group in /frontend-react with 1 update: [msw](https://github.com/mswjs/msw). Updates `msw` from 2.2.14 to 2.3.0 - [Release notes](https://github.com/mswjs/msw/releases) - [Changelog](https://github.com/mswjs/msw/blob/main/CHANGELOG.md) - [Commits](https://github.com/mswjs/msw/compare/v2.2.14...v2.3.0) --- updated-dependencies: - dependency-name: msw dependency-type: direct:development update-type: version-update:semver-minor dependency-group: msw ... Signed-off-by: dependabot[bot] * Bump @testing-library/react Bumps the testing-library group in /frontend-react with 1 update: [@testing-library/react](https://github.com/testing-library/react-testing-library). Updates `@testing-library/react` from 15.0.6 to 15.0.7 - [Release notes](https://github.com/testing-library/react-testing-library/releases) - [Changelog](https://github.com/testing-library/react-testing-library/blob/main/CHANGELOG.md) - [Commits](https://github.com/testing-library/react-testing-library/compare/v15.0.6...v15.0.7) --- updated-dependencies: - dependency-name: "@testing-library/react" dependency-type: direct:development update-type: version-update:semver-patch dependency-group: testing-library ... Signed-off-by: dependabot[bot] * Bump bridgecrewio/checkov-action from 12.2760.0 to 12.2762.0 (#14557) Bumps [bridgecrewio/checkov-action](https://github.com/bridgecrewio/checkov-action) from 12.2760.0 to 12.2762.0. - [Release notes](https://github.com/bridgecrewio/checkov-action/releases) - [Commits](https://github.com/bridgecrewio/checkov-action/compare/85a668b503d8267b34c43f8d8621f4bae915ce97...cbef505ba3282486a24541d7c862e19266ad0d96) --- updated-dependencies: - dependency-name: bridgecrewio/checkov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * 14457: only use bundle identifier for tracking element id (#14553) * 14457: only use bundle identifier for tracking element id * fixup! 14457: only use bundle identifier for tracking element id * Bump tj-actions/changed-files from 44.5.1 to 44.5.2 (#14565) Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 44.5.1 to 44.5.2. - [Release notes](https://github.com/tj-actions/changed-files/releases) - [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md) - [Commits](https://github.com/tj-actions/changed-files/compare/03334d095e2739fa9ac4034ec16f66d5d01e9eba...d6babd6899969df1a11d14c368283ea4436bca78) --- updated-dependencies: - dependency-name: tj-actions/changed-files dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump bridgecrewio/checkov-action from 12.2762.0 to 12.2765.0 (#14566) Bumps [bridgecrewio/checkov-action](https://github.com/bridgecrewio/checkov-action) from 12.2762.0 to 12.2765.0. - [Release notes](https://github.com/bridgecrewio/checkov-action/releases) - [Commits](https://github.com/bridgecrewio/checkov-action/compare/cbef505ba3282486a24541d7c862e19266ad0d96...329e2bb9c8d047eaa4216c2e815c6957cbf97e59) --- updated-dependencies: - dependency-name: bridgecrewio/checkov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update FHIRRouter Azure Events to Store messageID (#14541) * Updating related docs * Add bundle identifier to azure custom events * Fixing invalid fhir syntax * Adding new logging to FHIRDestinationFilter * code review feedback * 14249 add fhir inventory java class (#14355) * update hl7reader to use fhirinventory java class * update OBX, ORC, NTE mappings * test updates * message class updates * update end to end tests * update mapping inventory notes * Update HL7v2-FHIR-Inventory.md * Bump braces (#14491) * Bump lint-staged from 15.2.2 to 15.2.5 in /frontend-react Bumps [lint-staged](https://github.com/okonet/lint-staged) from 15.2.2 to 15.2.5. - [Release notes](https://github.com/okonet/lint-staged/releases) - [Changelog](https://github.com/lint-staged/lint-staged/blob/master/CHANGELOG.md) - [Commits](https://github.com/okonet/lint-staged/compare/v15.2.2...v15.2.5) --- updated-dependencies: - dependency-name: lint-staged dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump the playwright group across 1 directory with 2 updates Bumps the playwright group with 2 updates in the /frontend-react directory: [@playwright/test](https://github.com/microsoft/playwright) and [eslint-plugin-playwright](https://github.com/playwright-community/eslint-plugin-playwright). Updates `@playwright/test` from 1.43.1 to 1.44.1 - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.43.1...v1.44.1) Updates `eslint-plugin-playwright` from 1.6.0 to 1.6.2 - [Release notes](https://github.com/playwright-community/eslint-plugin-playwright/releases) - [Changelog](https://github.com/playwright-community/eslint-plugin-playwright/blob/main/CHANGELOG.md) - [Commits](https://github.com/playwright-community/eslint-plugin-playwright/compare/v1.6.0...v1.6.2) --- updated-dependencies: - dependency-name: "@playwright/test" dependency-type: direct:development update-type: version-update:semver-minor dependency-group: playwright - dependency-name: eslint-plugin-playwright dependency-type: direct:development update-type: version-update:semver-patch dependency-group: playwright ... Signed-off-by: dependabot[bot] * Bump @types/react from 18.3.1 to 18.3.3 in /frontend-react Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.3.1 to 18.3.3. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) --- updated-dependencies: - dependency-name: "@types/react" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump axios from 1.6.8 to 1.7.2 in /frontend-react Bumps [axios](https://github.com/axios/axios) from 1.6.8 to 1.7.2. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.6.8...v1.7.2) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * validated init module ignore changes * validate insights module ignore changes * validate endpoint module ignore changes * validate database module ignore changes * ignore changes validation * validate ignore_changes * remove front door ignore_changes * tf format * remove function app ignore_changes * update storage ignore_changes * update storage ignore_changes * update metabase tf * update ignore_changes * add checkov exceptions * hook scaffolding (#14340) * AdminTools remediations (#14607) * Admin remediations * Bump glob from 7.2.3 to 9.3.5 in /frontend-react (#14516) * manipulate lock instead of forcing resolution which allows 9.x.x and 10.x.x (but not 7.x.x) * remove stray requirements.txt (#14610) * 13124 - Updates to Nav bar to create auth sub nav. (#14558) * 13124 - Updates to Nav bar to create auth sub nav. * css updates * Updates to unit tests * Updates to e2e tests * Added bold class to daily data and submission menu items * When signed in as a receiver, send them to Daily Data * Fix mobile menu. * Added data-testid to auth header. * refactor code to remove unneeded event listeners * Added data-testid to auth header. * Fixed e2e test * Fixed import --------- Co-authored-by: etanb * Point schema to azure * Update setting. * Relocate the setting * Removed the nd-phd.yml from STLTs * Moved settings and added match result status F, C, P to transform * Updated the PR * Initial commit FL receiver UP migration (#14453) * Initial commit FL receiver UP migration * Update of change receive transfor element names * Updated Transform * Relocate the setting file * Relocate the setting * Experience/14762/get deliveries history api (#14909) * 14762 - Added new getDeliveriesHistory endpoint to use instead of the getDeliveries endpoint for Daily Data. * 14762 - Added unit tests * 14762 - Fixed lint errors * 14762 - Updates to unit tests. Removed orgService/receivingOrgSvc param since it's not needed since we pass it in as part of the organization. * 14762 - Updated fileName per PR #14649 * 14762 - added back receivingOrgSvc param * 14762 - Updates to unit tests Reverted ktlint formatting from files not associated with this PR * 14721: verify action params and SQL param escaping (#14889) * 14721: add more validation around sender ip and payloadname * Update cleanslate for new gradlew executable location * fixup! 14721: add more validation around sender ip and payloadname * fixup! 14721: add more validation around sender ip and payloadname * fixup! 14721: add more validation around sender ip and payloadname * fixup! 14721: add more validation around sender ip and payloadname * MA UP migration pre-work updated (#14916) * Bump postcss from 8.4.38 to 8.4.39 in /frontend-react Bumps [postcss](https://github.com/postcss/postcss) from 8.4.38 to 8.4.39. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.4.38...8.4.39) --- updated-dependencies: - dependency-name: postcss dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump @types/lodash from 4.17.5 to 4.17.6 in /frontend-react Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.5 to 4.17.6. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash) --- updated-dependencies: - dependency-name: "@types/lodash" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump @okta/okta-signin-widget in /frontend-react in the okta group Bumps the okta group in /frontend-react with 1 update: [@okta/okta-signin-widget](https://github.com/okta/okta-signin-widget). Updates `@okta/okta-signin-widget` from 7.19.4 to 7.19.6 - [Release notes](https://github.com/okta/okta-signin-widget/releases) - [Changelog](https://github.com/okta/okta-signin-widget/blob/master/webpack.release.config.js) - [Commits](https://github.com/okta/okta-signin-widget/compare/okta-signin-widget-7.19.4...okta-signin-widget-7.19.6) --- updated-dependencies: - dependency-name: "@okta/okta-signin-widget" dependency-type: direct:production update-type: version-update:semver-patch dependency-group: okta ... Signed-off-by: dependabot[bot] * detect root backend/frontend changes * North Dakota UP Receiver Migration Pre-Work (#14661) * North Dakota UP Receiver Migration Pre-Work * Correct the setting and move it to staging/setting * Rename the setting file * Move setting and add county code to the transform * Removed county code from transform * Updated the PR * Bump undici from 6.19.0 to 6.19.2 in /frontend-react Bumps [undici](https://github.com/nodejs/undici) from 6.19.0 to 6.19.2. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v6.19.0...v6.19.2) --- updated-dependencies: - dependency-name: undici dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump sass from 1.77.5 to 1.77.6 in /frontend-react Bumps [sass](https://github.com/sass/dart-sass) from 1.77.5 to 1.77.6. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.77.5...1.77.6) --- updated-dependencies: - dependency-name: sass dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Ignore linting/testing steps in build-frontend when in trialfrontend env (#14976) * Ignore linting/testing steps in build-frontend when in trialfrontend env Fixes #14975 * Added SuppressNonNPI to the PR * Use default SFTP --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: etanb Co-authored-by: Michael Kalish Co-authored-by: Josh Fisk Co-authored-by: Jack Wang Co-authored-by: Stephen Nesman <94193373+snesm@users.noreply.github.com> Co-authored-by: Josiah Siegel Co-authored-by: Joseph Andersen <12385932+jpandersen87@users.noreply.github.com> Co-authored-by: Penelope Lischer <102491809+penny-lischer@users.noreply.github.com> * Bump bridgecrewio/checkov-action from 12.2824.0 to 12.2826.0 (#15253) Bumps [bridgecrewio/checkov-action](https://github.com/bridgecrewio/checkov-action) from 12.2824.0 to 12.2826.0. - [Release notes](https://github.com/bridgecrewio/checkov-action/releases) - [Commits](https://github.com/bridgecrewio/checkov-action/compare/fa45bce4384650003ab4f450b022372c3c13ef75...18feed40df08cb77a3c9f2933d98fe46ed56c7e9) --- updated-dependencies: - dependency-name: bridgecrewio/checkov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * update repo atmoz/sftp image * migrate to github repo for sftp image * Bump tj-actions/changed-files from 44.5.5 to 44.5.6 (#15266) Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 44.5.5 to 44.5.6. - [Release notes](https://github.com/tj-actions/changed-files/releases) - [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md) - [Commits](https://github.com/tj-actions/changed-files/compare/cc733854b1f224978ef800d29e4709d5ee2883e4...6b2903bdce6310cfbddd87c418f253cf29b2dec9) --- updated-dependencies: - dependency-name: tj-actions/changed-files dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix broken support link * Experience/15057/e2e smoke tests (#15207) * 14210 - spike to show how to switch b/w mock and server data for e2e tests. * BasePage tests * simplify BasePage lifecycle * 14210 - updates * fix org setting POM * 14210 - Updated README.md * merge fixes * merge fixes * add mfa select logic for e2e auth * 14654 - fixed duplicate nav entries * 14210 - Added smoke test attribute to tests * 14210 - Removed smoke test attribute from daily data details since we will create a smoke user flow instead. * 14210 - Added logic to only run smoke tests against Chrome browser. * 14210 - Reverted submissions-history-page.spec * fix isPageLoadExpected on receiver status * remove now localestring comparison * 14794 - Added comments to BasePage.ts. Moved smoke attribute to tests that don't require user flow. * 15057 - Updated smoke tests --------- Co-authored-by: Joseph Andersen <12385932+jpandersen87@users.noreply.github.com> * 14794 - Added Managing Your Connection e2e. (#15259) * 14794 - Added Managing Your Connection e2e. Updates to other e2e spec files. * 14794 - fixed bug and irrelevant eslint disable comment * OK UP Migration Validate Test Data (#15261) * OK UP Migration Validate Test Data * Update integration test * Spring Submissions API (#15187) * add SubmissionController with validateHeaders function * add CreationResponse * add IEvent and QueueAccess to shared module * fix toTaskAction usages * update package name * add azure storage config * add azure table insert * remove IEvent and QueueAccess from share project * add azure table upload * add SubmissionControllerTest * update gradle to exclude some logging * fix lint failures * test commit * attempt custom message convert * first test working without custom content-type * fix tests * tests are working! * import UUID * get app insights event working * connection string * remove app insight conn string * remove instrumentation key * add logging * add clientID to the ReportReceivedEvent * add documentation to functions and a default connection string * remove file * undo formatting changes * change the config Bean to BlobContainerClient, QueueClient, and TableClient * review fixup * convert from offsetdatetime to instant * more fix ups * add access to azure table * convert from telemetry client to telemetry service * add integration test * add io exception handler and move queue message upload to end of processing * migrate to github repo for sftp alpine image * 15059 - added github action workflow for frontend smoke tests to be run against live data (#15235) * 15059 - added github action workflow for frontend smoke tests to be run against live data * 15059 - removed env param * Bump bridgecrewio/checkov-action from 12.2826.0 to 12.2829.0 (#15289) Bumps [bridgecrewio/checkov-action](https://github.com/bridgecrewio/checkov-action) from 12.2826.0 to 12.2829.0. - [Release notes](https://github.com/bridgecrewio/checkov-action/releases) - [Commits](https://github.com/bridgecrewio/checkov-action/compare/18feed40df08cb77a3c9f2933d98fe46ed56c7e9...8b982845c18d56df5f5b290ae30b6857e7abfb00) --- updated-dependencies: - dependency-name: bridgecrewio/checkov-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump operations/slack-boltjs-app from `da24bed` to `f2fdbae` (#15286) Bumps [operations/slack-boltjs-app](https://github.com/JosiahSiegel/slack-boltjs-app) from `da24bed` to `f2fdbae`. - [Release notes](https://github.com/JosiahSiegel/slack-boltjs-app/releases) - [Commits](https://github.com/JosiahSiegel/slack-boltjs-app/compare/da24bed12bf449394ea84bc266402fb0169be239...f2fdbaeee3805fec7ed27ea89df236e6b91ce149) --- updated-dependencies: - dependency-name: operations/slack-boltjs-app dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stephen Nesman <94193373+snesm@users.noreply.github.com> * Bump docker/login-action from 3.2.0 to 3.3.0 (#15287) Bumps [docker/login-action](https://github.com/docker/login-action) from 3.2.0 to 3.3.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/0d4c9c5ea7693da7b068278f7b52bda2a190a446...9780b0c442fbb1117ed29e0efdff1e18412f7567) --- updated-dependencies: - dependency-name: docker/login-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * migrate staging legacy sftp to local repo image to avoid docker auth issues * prettier errors (#15297) Fixes #15296 * e2e test for refer healthcare * Update playwright linting rules (#15299) * Update playwright linting rules Fixes #15298 * Move OK PHD UP move to production (#15290) * Move OK PHD UP move to production * Remove MPOX for now --------- Signed-off-by: dependabot[bot] Co-authored-by: Ott Sathngam Co-authored-by: Basilio Bogado <541149+basiliskus@users.noreply.github.com> Co-authored-by: JessicaWNava <119880261+JessicaWNava@users.noreply.github.com> Co-authored-by: Stephen Nesman <94193373+snesm@users.noreply.github.com> Co-authored-by: Josiah Siegel Co-authored-by: Gabriel Dorsch Co-authored-by: Josiah Siegel <5522990+JosiahSiegel@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: etanb Co-authored-by: Michael Kalish Co-authored-by: Josh Fisk Co-authored-by: Jack Wang Co-authored-by: Joseph Andersen <12385932+jpandersen87@users.noreply.github.com> Co-authored-by: Penelope Lischer <102491809+penny-lischer@users.noreply.github.com> Co-authored-by: etanb Co-authored-by: Brick Green <86254221+brick-green@users.noreply.github.com> --- .editorconfig | 67 +- .github/actions/build-backend/action.yml | 2 +- .github/workflows/publish_docker.yaml | 116 +- .github/workflows/snyk.yml | 2 +- .github/workflows/sonarcloud.yml | 4 +- .github/workflows/start_frontend_smoke.yml | 37 + .github/workflows/validate_terraform.yml | 2 +- ...eportstream.project-conventions.gradle.kts | 20 +- frontend-react/.eslintrc.cjs | 43 +- frontend-react/404.html | 15 +- frontend-react/README.md | 11 +- frontend-react/e2e/helpers/auth.setup.ts | 7 + frontend-react/e2e/pages/BasePage.ts | 245 ++-- .../e2e/pages/about-side-navigation.ts | 5 +- frontend-react/e2e/pages/about.ts | 2 +- .../e2e/pages/admin/receiver-status.ts | 78 +- .../e2e/pages/daily-data-details.ts | 100 +- frontend-react/e2e/pages/daily-data.ts | 25 + .../e2e/pages/managing-your-connection.ts | 16 + frontend-react/e2e/pages/organization.ts | 74 +- frontend-react/e2e/pages/refer-healthcare.ts | 16 + frontend-react/e2e/pages/support.ts | 1 + frontend-react/e2e/spec/all/BasePage.spec.ts | 104 ++ .../e2e/spec/all/about-page.spec.ts | 3 +- .../all/admin/receiver-status-page.spec.ts | 384 ++++--- .../spec/all/daily-data-details-page.spec.ts | 376 ++++-- .../e2e/spec/all/daily-data-page.spec.ts | 6 +- frontend-react/e2e/spec/all/homepage.spec.ts | 118 +- .../e2e/spec/all/idletimeout.spec.ts | 57 +- .../all/managing-your-connection-page.spec.ts | 124 ++ .../all/organization-settings-page.spec.ts | 468 ++++---- .../e2e/spec/all/our-network-page.spec.ts | 78 +- .../spec/all/refer-healthcare-page.spec.ts | 85 ++ frontend-react/e2e/spec/all/roadmap.spec.ts | 235 ++-- .../e2e/spec/all/support-page.spec.ts | 28 +- frontend-react/e2e/spec/all/timezone.spec.ts | 1 - frontend-react/e2e/test.ts | 5 +- frontend-react/index.html | 20 +- frontend-react/lint-staged.config.js | 14 +- frontend-react/package.json | 402 +++---- frontend-react/playwright.config.ts | 4 +- frontend-react/src/content/support/index.mdx | 2 +- frontend-react/tsconfig.json | 52 +- frontend-react/tsconfig.node.json | 16 +- frontend-react/unsupported-browser.html | 68 +- frontend-react/vite.config.ts | 30 +- .../app/terraform/modules/common/sftp/main.tf | 2 +- .../modules/container_registry/~outputs.tf | 4 + .../terraform/modules/sftp_container/main.tf | 21 +- operations/docker-compose.yml | 2 - operations/slack-boltjs-app | 2 +- prime-router/build.gradle.kts | 2 - prime-router/docker-compose.build.yml | 1 - prime-router/docker-compose.postgres.yml | 3 +- prime-router/docker-compose.yml | 2 +- .../configuring-filters.md | 36 + .../docs/universal-pipeline/README.md | 21 +- .../universal-pipeline/destination-filter.md | 197 ++++ .../universal-pipeline/receiver-filter.md | 303 +++++ prime-router/docs/universal-pipeline/route.md | 2 +- prime-router/settings/STLTs/AL/al-phl.yml | 2 +- .../settings/STLTs/Flexion/flexion.yml | 4 +- prime-router/settings/STLTs/IL/il-phd.yml | 105 ++ prime-router/settings/STLTs/LA/la-ochsner.yml | 3 +- prime-router/settings/STLTs/LA/la-phl.yml | 2 +- prime-router/settings/STLTs/MS/ms-doh.yml | 102 ++ prime-router/settings/STLTs/NM/nm-doh.yml | 98 ++ prime-router/settings/STLTs/OK/ok-phd.yml | 8 +- .../settings/STLTs/Oracle/oracle-rln.yml | 3 +- prime-router/settings/STLTs/VT/vt-doh.yml | 4 +- .../src/main/kotlin/ReportStreamFilter.kt | 5 +- prime-router/src/main/kotlin/azure/Event.kt | 2 +- .../observability/event/AzureEventService.kt | 17 + .../kotlin/fhirengine/azure/FHIRFunctions.kt | 8 +- .../kotlin/fhirengine/engine/FHIRConverter.kt | 6 +- .../kotlin/fhirengine/engine/FHIRRouter.kt | 1 + .../fhirengine/utils/FHIRBundleHelpers.kt | 4 +- .../src/main/kotlin/report/ReportService.kt | 5 +- .../receivers/enrichments/vt-enrichment.yml | 1 - ...LabOrder.yml => etor-sender-transform.yml} | 0 ...er.yml => etor-oml-receiver-transform.yml} | 0 .../STLTs/IL/IL-receiver-transform.yml | 67 ++ .../STLTs/MS/MS-receiver-tranform.yml | 62 + .../STLTs/NM/NM-receiver-transform.yml | 55 + .../STLTs/OK/OK-receiver-transform.yml | 30 + .../common/UniversalPipelineTestUtils.kt | 274 +++++ .../azure/FHIRConverterIntegrationTests.kt | 51 +- .../FHIRDestinationFilterIntegrationTests.kt | 406 +++++++ .../FHIRReceiverFilterIntegrationTests.kt | 1022 +++++++++++++++++ .../azure/FHIRRouterIntegrationTests.kt | 2 +- .../azure/FhirFunctionIntegrationTests.kt | 203 ++-- .../fhirengine/azure/FhirFunctionTests.kt | 15 +- .../fhirRouterTests/DefaultFilterTests.kt | 1 + .../engine/fhirRouterTests/GetFilterTests.kt | 1 + .../kotlin/history/SubmissionHistoryTests.kt | 281 ++++- .../SubmissionFunctionIntegrationTests.kt | 238 ++++ .../test/kotlin/report/ReportServiceTests.kt | 8 +- .../engine/bundle_multiple_observations.fhir | 2 +- .../FHIR_to_HL7/sample_NM_20240702-0001.fhir | 1 + .../FHIR_to_HL7/sample_NM_20240702-0001.hl7 | 12 + .../FHIR_to_HL7/sample_OK_20240628-0001.hl7 | 2 +- .../datatests/translation-test-config.csv | 7 +- .../resources/settings/organizations.yml | 103 ++ .../shared/SubmissionQueueMessage.kt | 5 + submissions/build.gradle.kts | 10 + .../submissions/CustomMediaTypes.kt | 8 + .../submissions/ReportReceivedEvent.kt | 16 + .../submissions/SubmissionsApplication.kt | 2 + .../submissions/TelemetryService.kt | 30 + .../reportstream/submissions/WebConfig.kt | 25 + .../submissions/config/AzureConfig.kt | 56 + .../controllers/SubmissionController.kt | 247 ++++ .../src/main/resources/application.properties | 4 + .../SubmissionControllerIntegrationTest.kt | 134 +++ .../test/kotlin/SubmissionControllerTest.kt | 334 ++++++ .../resources/application-test.properties | 3 + 116 files changed, 6771 insertions(+), 1492 deletions(-) create mode 100644 .github/workflows/start_frontend_smoke.yml create mode 100644 frontend-react/e2e/pages/refer-healthcare.ts create mode 100644 frontend-react/e2e/spec/all/BasePage.spec.ts create mode 100644 frontend-react/e2e/spec/all/managing-your-connection-page.spec.ts create mode 100644 frontend-react/e2e/spec/all/refer-healthcare-page.spec.ts create mode 100644 prime-router/docs/standard-operating-procedures/configuring-filters.md create mode 100644 prime-router/docs/universal-pipeline/destination-filter.md create mode 100644 prime-router/docs/universal-pipeline/receiver-filter.md create mode 100644 prime-router/settings/STLTs/IL/il-phd.yml create mode 100644 prime-router/settings/STLTs/MS/ms-doh.yml create mode 100644 prime-router/settings/STLTs/NM/nm-doh.yml delete mode 100644 prime-router/src/main/resources/metadata/fhir_transforms/receivers/enrichments/vt-enrichment.yml rename prime-router/src/main/resources/metadata/fhir_transforms/senders/Flexion/{TILabOrder.yml => etor-sender-transform.yml} (100%) rename prime-router/src/main/resources/metadata/hl7_mapping/receivers/Flexion/{TILabOrder.yml => etor-oml-receiver-transform.yml} (100%) create mode 100644 prime-router/src/main/resources/metadata/hl7_mapping/receivers/STLTs/IL/IL-receiver-transform.yml create mode 100644 prime-router/src/main/resources/metadata/hl7_mapping/receivers/STLTs/MS/MS-receiver-tranform.yml create mode 100644 prime-router/src/main/resources/metadata/hl7_mapping/receivers/STLTs/NM/NM-receiver-transform.yml create mode 100644 prime-router/src/test/kotlin/fhirengine/azure/FHIRDestinationFilterIntegrationTests.kt create mode 100644 prime-router/src/test/kotlin/fhirengine/azure/FHIRReceiverFilterIntegrationTests.kt create mode 100644 prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_NM_20240702-0001.fhir create mode 100644 prime-router/src/testIntegration/resources/datatests/FHIR_to_HL7/sample_NM_20240702-0001.hl7 create mode 100644 shared/src/main/kotlin/gov/cdc/prime/reportstream/shared/SubmissionQueueMessage.kt create mode 100644 submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/CustomMediaTypes.kt create mode 100644 submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/ReportReceivedEvent.kt create mode 100644 submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/TelemetryService.kt create mode 100644 submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/WebConfig.kt create mode 100644 submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/config/AzureConfig.kt create mode 100644 submissions/src/main/kotlin/gov/cdc/prime/reportstream/submissions/controllers/SubmissionController.kt create mode 100644 submissions/src/test/kotlin/SubmissionControllerIntegrationTest.kt create mode 100644 submissions/src/test/kotlin/SubmissionControllerTest.kt create mode 100644 submissions/src/test/resources/application-test.properties diff --git a/.editorconfig b/.editorconfig index 9ecf93e5f66..574eb5cff8c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,10 +1,73 @@ +# Dump from Jet Brain's defaults. Only really care about Kotlin Code Style standards [*] charset = utf-8 end_of_line = lf indent_size = 4 indent_style = space insert_final_newline = false +max_line_length = 120 tab_width = 4 -[{*.yaml,*.yml, *.tf}] -indent_size = 2 \ No newline at end of file +[*.conf] +indent_size = 2 +tab_width = 2 + +[*.java] +indent_size = 2 +tab_width = 2 + +[*.less] +indent_size = 2 + +[*.sass] +indent_size = 2 + +[*.scss] +indent_size = 2 + +[*.styl] +indent_size = 2 + +[{*.bash,*.sh,*.zsh}] +indent_size = 2 +tab_width = 2 + + +# follow the official Kotlin Style Guide +# ktlint is more strict, so we disable some of those rules +[{*.gradle.kts,*.kt,*.kts,*.main.kts}] +indent_size = 4 +indent_style = space +ktlint_code_style = intellij_idea +# the following rule prevented EOL comments in some places where it is useful +ktlint_standard_discouraged-comment-location = disabled +# version 1.1.1 allow same line comments +ktlint_standard_value-argument-comment = disabled +ktlint_standard_value-parameter-comment = disabled +# the following rule was required to disable `discouraged-comment-location` +ktlint_standard_if-else-wrapping = disabled +# the following rule is unnecessarily strict about comment placement and type +ktlint_standard_comment-wrapping = disabled +# the following rule is disabled to leave trailing commas at call site optional +ktlint_standard_trailing-comma-on-call-site = disabled +# the following rule has too many edge cases that conflict with the built-in formatter +ktlint_standard_indent = disabled +# the following rule is unnecessarily strict and can eat up a lot of vertical space +ktlint_standard_argument-list-wrapping = disabled +# the following rule unnecessarily prevents certain forms of listing parameters +ktlint_standard_function-signature = disabled +# the following rules can be re-enabled in the future with manual changes and sufficient testing +ktlint_standard_property-naming = disabled +ktlint_standard_enum-entry-name-case = disabled +ktlint_standard_function-naming = disabled +ktlint_standard_filename = disabled + +[{*.har,*.jsb2,*.jsb3,*.json,*.fhir,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}] +indent_size = 2 + + +[{*.yaml,*.yml}] +indent_size = 2 + +[{*.schema,*.valueset}] +indent_size = 2 diff --git a/.github/actions/build-backend/action.yml b/.github/actions/build-backend/action.yml index 28c552fd28b..37b26be53ac 100644 --- a/.github/actions/build-backend/action.yml +++ b/.github/actions/build-backend/action.yml @@ -39,7 +39,7 @@ runs: distribution: "temurin" cache: "gradle" - - uses: gradle/actions/wrapper-validation@dbbdc275be76ac10734476cc723d82dfe7ec6eda + - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 - name: Lint if: inputs.run-integration-tests == 'true' diff --git a/.github/workflows/publish_docker.yaml b/.github/workflows/publish_docker.yaml index 44b092d9b27..542075aa950 100644 --- a/.github/workflows/publish_docker.yaml +++ b/.github/workflows/publish_docker.yaml @@ -1,4 +1,4 @@ -name: Publish Container to GitHub +name: Publish Images to GitHub on: workflow_dispatch: @@ -10,6 +10,7 @@ on: env: REGISTRY: ghcr.io + SFTP_IMAGE_NAME: cdcgov/prime-reportstream_sftp jobs: pre_job: @@ -17,7 +18,9 @@ jobs: runs-on: ubuntu-latest outputs: has_tfcli_change: ${{ steps.skip_check.outputs.tfcli && github.event_name != 'schedule'}} - has_dnsmasq_change: ${{ steps.skip_check.outputs.dnsmasq || github.event_name == 'schedule'}} + has_dnsmasq_change: ${{ steps.skip_check.outputs.dnsmasq || github.event_name == 'schedule'}} + run_publish_sftp: ${{ steps.check_sftp_image.outputs.run_job }} + run_publish_sftp_alpine: ${{ steps.check_sftp_alpine_image.outputs.run_job }} steps: - name: "Check out changes" uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 @@ -26,32 +29,70 @@ jobs: with: list-files: csv filters: | - tfcli: - - 'operations/docker-compose.yml' - - 'operations/Dockerfile' - - '.github/workflows/build_docker.yml' dnsmasq: - 'operations/dnsmasq/**' - publish_tfcli: - name: Publish Terraform CLI + - name: Log In to the Container Registry + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Check sftp image + id: check_sftp_image + run: | + docker pull atmoz/sftp + docker pull ${{ env.REGISTRY }}/${{ env.SFTP_IMAGE_NAME }} + LATEST_IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' atmoz/sftp) + REPO_IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ${{ env.REGISTRY }}/${{ env.SFTP_IMAGE_NAME }} 2> /dev/null || true) + + if [ "$LATEST_IMAGE_DIGEST" != "$REPO_IMAGE_DIGEST" ]; then + echo "image outdated" + echo "run_job=true" >> $GITHUB_OUTPUT + else + echo "image current" + echo "run_job=false" >> $GITHUB_OUTPUT + fi + + - name: Check sftp alpine image + id: check_sftp_alpine_image + run: | + docker pull atmoz/sftp:alpine + docker pull ${{ env.REGISTRY }}/${{ env.SFTP_IMAGE_NAME }}:alpine + LATEST_IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' atmoz/sftp:alpine) + REPO_IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ${{ env.REGISTRY }}/${{ env.SFTP_IMAGE_NAME }}:alpine 2> /dev/null || true) + + if [ "$LATEST_IMAGE_DIGEST" != "$REPO_IMAGE_DIGEST" ]; then + echo "image outdated" + echo "run_job=true" >> $GITHUB_OUTPUT + else + echo "image current" + echo "run_job=false" >> $GITHUB_OUTPUT + fi + + publish_dnsmasq: + name: Publish dnsmasq needs: pre_job - if: ${{ needs.pre_job.outputs.has_tfcli_change == 'true'}} + if: ${{ needs.pre_job.outputs.has_dnsmasq_change == 'true' }} runs-on: ubuntu-latest defaults: run: - working-directory: operations + working-directory: operations/dnsmasq env: - IMAGE_NAME: cdcgov/prime-reportstream_tfcli + IMAGE_NAME: cdcgov/prime-reportstream_dnsmasq permissions: contents: read packages: write + strategy: + matrix: + AZ_ENV: [ demo1, demo2, demo3, test, staging, prod ] steps: - name: Check Out Changes uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 - name: Log In to the Container Registry - uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -59,28 +100,48 @@ jobs: - name: Build Docker Terraform CLI run: | - make build-tf-cli + docker build --build-arg AZ_ENV=${{ matrix.AZ_ENV }} -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.AZ_ENV }} . - name: Push to the Container Registry run: | docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} --all-tags - publish_dnsmasq: - name: Publish dnsmasq + publish_sftp: + name: Publish SFTP needs: pre_job - if: ${{ needs.pre_job.outputs.has_dnsmasq_change == 'true' }} + if: ${{ needs.pre_job.outputs.run_publish_sftp == 'true' }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Check Out Changes + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 + + - name: Log In to the Container Registry + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Tag Image + run: | + docker pull atmoz/sftp + docker tag atmoz/sftp ${{ env.REGISTRY }}/${{ env.SFTP_IMAGE_NAME }} + + - name: Push to the Container Registry + run: | + docker push ${{ env.REGISTRY }}/${{ env.SFTP_IMAGE_NAME }} --all-tags + + publish_sftp_alpine: + name: Publish SFTP Alpine + needs: pre_job + if: ${{ needs.pre_job.outputs.run_publish_sftp_alpine == 'true' }} runs-on: ubuntu-latest - defaults: - run: - working-directory: operations/dnsmasq - env: - IMAGE_NAME: cdcgov/prime-reportstream_dnsmasq permissions: contents: read packages: write - strategy: - matrix: - AZ_ENV: [ demo1, demo2, demo3, test, staging, prod ] steps: - name: Check Out Changes uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 @@ -92,10 +153,11 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build Docker Terraform CLI + - name: Tag Image run: | - docker build --build-arg AZ_ENV=${{ matrix.AZ_ENV }} -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.AZ_ENV }} . + docker pull atmoz/sftp:alpine + docker tag atmoz/sftp:alpine ${{ env.REGISTRY }}/${{ env.SFTP_IMAGE_NAME }}:alpine - name: Push to the Container Registry run: | - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} --all-tags + docker push ${{ env.REGISTRY }}/${{ env.SFTP_IMAGE_NAME }}:alpine \ No newline at end of file diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 3ec146e7351..225ca82d7b9 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -37,7 +37,7 @@ jobs: java-version: "17" distribution: "temurin" cache: "gradle" - - uses: gradle/actions/wrapper-validation@dbbdc275be76ac10734476cc723d82dfe7ec6eda + - uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 - name: Snyk Monitor run: snyk monitor --org=prime-reportstream env: diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 9eac6633379..841dd2b1a09 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -52,7 +52,7 @@ jobs: - name: "Get changed files with yaml" id: changed-files-yaml - uses: tj-actions/changed-files@cc733854b1f224978ef800d29e4709d5ee2883e4 + uses: tj-actions/changed-files@6b2903bdce6310cfbddd87c418f253cf29b2dec9 with: files_yaml: | frontend: @@ -70,7 +70,7 @@ jobs: - name: Gradle Validation if: steps.changed-files-yaml.outputs.backend_any_changed == 'true' || steps.branch-name.outputs.is_default == 'true' - uses: gradle/actions/wrapper-validation@dbbdc275be76ac10734476cc723d82dfe7ec6eda + uses: gradle/actions/wrapper-validation@d9c87d481d55275bb5441eef3fe0e46805f9ef70 - name: Spin up build containers if: steps.changed-files-yaml.outputs.backend_any_changed == 'true' || steps.branch-name.outputs.is_default == 'true' diff --git a/.github/workflows/start_frontend_smoke.yml b/.github/workflows/start_frontend_smoke.yml new file mode 100644 index 00000000000..ef3f5116e67 --- /dev/null +++ b/.github/workflows/start_frontend_smoke.yml @@ -0,0 +1,37 @@ +name: "Start Frontend Smoke Tests" + +on: + workflow_dispatch: + inputs: + username: + description: 'Administrator username for the environment chosen:' + required: true + type: string + password: + description: 'Administrator password for the environment chosen:' + required: true + type: string + +jobs: + run_smoke: + name: Frontend Smoke Tests + runs-on: ubuntu-latest + + steps: + - name: "Run frontend smoke tests" + working-directory: frontend-react + env: + TEST_ADMIN_USERNAME: ${{ github.event.inputs.username }} + TEST_ADMIN_PASSWORD: ${{ github.event.inputs.password }} + run: | + echo "::group::E2E smoke tests" + yarn run test:e2e-smoke + echo "::endgroup::" + shell: bash + + - name: "Store E2E Results" + uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b + with: + name: e2e-data + path: frontend-react/e2e-data/ + retention-days: 7 \ No newline at end of file diff --git a/.github/workflows/validate_terraform.yml b/.github/workflows/validate_terraform.yml index 7a5ef086c6c..d2032961f18 100644 --- a/.github/workflows/validate_terraform.yml +++ b/.github/workflows/validate_terraform.yml @@ -48,7 +48,7 @@ jobs: uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 - name: Run Checkov action - uses: bridgecrewio/checkov-action@fa45bce4384650003ab4f450b022372c3c13ef75 + uses: bridgecrewio/checkov-action@8b982845c18d56df5f5b290ae30b6857e7abfb00 with: directory: operations/app/terraform skip_check: CKV_AZURE_139,CKV_AZURE_137,CKV_AZURE_103,CKV_AZURE_104,CKV_AZURE_102,CKV_AZURE_130,CKV_AZURE_121,CKV_AZURE_67,CKV_AZURE_56,CKV_AZURE_17,CKV_AZURE_63,CKV_AZURE_18,CKV_AZURE_88,CKV_AZURE_65,CKV_AZURE_13,CKV_AZURE_66,CKV_AZURE_33,CKV_AZURE_35,CKV_AZURE_36,CKV_AZURE_98,CKV2_AZURE_1,CKV2_AZURE_15,CKV2_AZURE_21,CKV_AZURE_213,CKV_AZURE_59,CKV2_AZURE_33,CKV2_AZURE_32,CKV2_AZURE_28,CKV_AZURE_206,CKV_AZURE_42,CKV_AZURE_110,CKV_AZURE_109,CKV_AZURE_166,CKV2_AZURE_38,CKV2_AZURE_40,CKV2_AZURE_41,CKV_AZURE_235 diff --git a/buildSrc/src/main/kotlin/reportstream.project-conventions.gradle.kts b/buildSrc/src/main/kotlin/reportstream.project-conventions.gradle.kts index 0f4b755e4c1..34c9d7b3d99 100644 --- a/buildSrc/src/main/kotlin/reportstream.project-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/reportstream.project-conventions.gradle.kts @@ -17,9 +17,6 @@ kotlin { jvmToolchain(17) } - - - val majorJavaVersion = 17 java { sourceCompatibility = JavaVersion.toVersion(majorJavaVersion) @@ -50,7 +47,24 @@ dependencies { // Common dependencies implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion") + implementation("com.azure:azure-core:1.49.0") + implementation("com.azure:azure-core-http-netty:1.15.0") + implementation("com.azure:azure-data-tables:12.2.0") + implementation("com.azure:azure-storage-queue:12.21.0") { + exclude(group = "com.azure", module = "azure-core") + } + implementation("com.azure:azure-storage-blob:12.26.0") { + exclude(group = "com.azure", module = "azure-core") + } + implementation("com.microsoft.azure:applicationinsights-core:3.5.3") + implementation("org.apache.logging.log4j:log4j-api:2.23.1") + implementation("org.apache.logging.log4j:log4j-core:2.23.1") + implementation("org.apache.logging.log4j:log4j-slf4j2-impl:2.23.1") + implementation("org.apache.logging.log4j:log4j-layout-template-json:2.23.1") + implementation("org.apache.logging.log4j:log4j-api-kotlin:1.4.0") + implementation("io.github.oshai:kotlin-logging-jvm:6.0.9") // Common test dependencies testImplementation(kotlin("test-junit5")) diff --git a/frontend-react/.eslintrc.cjs b/frontend-react/.eslintrc.cjs index 45385778282..4a4debd4a41 100644 --- a/frontend-react/.eslintrc.cjs +++ b/frontend-react/.eslintrc.cjs @@ -33,14 +33,7 @@ module.exports = { project: true, tsconfigRootDir: __dirname, }, - plugins: [ - "react-refresh", - "@typescript-eslint", - "react-hooks", - "react", - "jsx-a11y", - "import", - ], + plugins: ["react-refresh", "@typescript-eslint", "react-hooks", "react", "jsx-a11y", "import"], settings: { react: { version: "detect", @@ -53,10 +46,7 @@ module.exports = { overrides: [ /* Vitest */ { - files: [ - "./src/**/__tests__/**/*.[jt]s?(x)", - "./src/**/?(*.)+(spec|test).[jt]s?(x)", - ], + files: ["./src/**/__tests__/**/*.[jt]s?(x)", "./src/**/?(*.)+(spec|test).[jt]s?(x)"], extends: [ "plugin:testing-library/react", "plugin:vitest/legacy-recommended", @@ -72,10 +62,7 @@ module.exports = { "@typescript-eslint/unbound-method": "off", /* Custom project rules */ - "testing-library/no-await-sync-events": [ - "error", - { eventModules: ["fire-event"] }, - ], + "testing-library/no-await-sync-events": ["error", { eventModules: ["fire-event"] }], "testing-library/no-render-in-lifecycle": "error", "testing-library/prefer-screen-queries": "warn", "testing-library/no-unnecessary-act": "warn", @@ -91,15 +78,17 @@ module.exports = { { files: ["./e2e/**/?(*.)+(spec|test).[jt]s"], extends: ["plugin:playwright/recommended"], + rules: { + // TODO: investigate these for reconsideration or per-module ignoring + "playwright/no-conditional-in-test": ["off"], + "playwright/no-force-option": ["off"], + }, }, ], rules: { /* Temporarily changed to warnings or disabled pending future work */ "jsx-a11y/no-autofocus": ["warn"], - "react-refresh/only-export-components": [ - "off", - { allowConstantExport: true }, - ], + "react-refresh/only-export-components": ["off", { allowConstantExport: true }], // Requires extensive updates to types in code, however SHOULD BE ENABLED EVENTUALLY "react/prop-types": ["warn"], @@ -134,14 +123,7 @@ module.exports = { "import/order": [ 1, { - groups: [ - "external", - "builtin", - "internal", - "sibling", - "parent", - "index", - ], + groups: ["external", "builtin", "internal", "sibling", "parent", "index"], pathGroups: [ { pattern: "components", group: "internal" }, { pattern: "common", group: "internal" }, @@ -156,10 +138,7 @@ module.exports = { alphabetize: { order: "asc", caseInsensitive: true }, }, ], - "sort-imports": [ - "error", - { ignoreCase: true, ignoreDeclarationSort: true }, - ], + "sort-imports": ["error", { ignoreCase: true, ignoreDeclarationSort: true }], "@typescript-eslint/prefer-nullish-coalescing": ["error"], }, }; diff --git a/frontend-react/404.html b/frontend-react/404.html index a606427f7ef..6e352fe8836 100644 --- a/frontend-react/404.html +++ b/frontend-react/404.html @@ -7,14 +7,8 @@ %VITE_TITLE% - - + + @@ -26,10 +20,7 @@ display: none; } - +
diff --git a/frontend-react/README.md b/frontend-react/README.md index 419ca9bf543..568f8a060d2 100644 --- a/frontend-react/README.md +++ b/frontend-react/README.md @@ -56,8 +56,10 @@ yarn run storybook # Runs a local instance of Storybook showcase of all of the c yarn run lint # Runs the front-end linter yarn run lint:fix # Runs the front-end linter and fixes style errors -yarn run test:e2e-ui # Runs a local instance of Playwright UI where you can view and run the e2e tests +yarn run test:e2e-ui # Runs a local instance of Playwright UI where you can view and run the e2e tests. This will run using mock data. CI=true yarn run test:e2e-ui # Runs a local instance of Playwright UI that mimics Github integration + +yarn run test:e2e-smoke # Runs the e2e tests that have the tag = @smoke and are meant to run against non mock data. ``` ## Static build info @@ -140,9 +142,12 @@ npx playwright install # Installs supported default browsers npx playwright install-deps # Installs system dependencies -yarn run test:e2e-ui # Runs a local instance of Playwright UI where you can view and run the e2e tests +yarn run test:e2e-ui # Runs a local instance of Playwright UI where you can view and run the e2e tests. This will run using mock data. + +CI=true yarn run test:e2e-ui # Runs a local instance of Playwright UI that mimics Github integration. + +yarn run test:e2e-smoke # Runs the e2e tests that have the tag = @smoke and are meant to run against non mock data. -CI=true yarn run test:e2e-ui # Runs a local instance of Playwright UI that mimics Github integration ``` Currently, the tests are running each time a pull request is made and must pass before the pull request can be merged into master. diff --git a/frontend-react/e2e/helpers/auth.setup.ts b/frontend-react/e2e/helpers/auth.setup.ts index 93a70d7d995..e82ff5f0da1 100644 --- a/frontend-react/e2e/helpers/auth.setup.ts +++ b/frontend-react/e2e/helpers/auth.setup.ts @@ -36,6 +36,13 @@ async function logIntoOkta(page: Page, login: TestLogin) { .or(page.getByRole("button", { name: "Verify" })); await btnSubmit.click(); + await expect(btnSubmit).not.toBeAttached(); + + const totpSelect = page.getByLabel("Select Google Authenticator."); + if (await totpSelect.isVisible()) { + await totpSelect.click(); + } + if (login.totpCode !== "" && login.totpCode !== undefined) { await page.getByLabel("Enter Code ").fill(totp.generate()); await page.getByRole("button", { name: "Verify" }).click(); diff --git a/frontend-react/e2e/pages/BasePage.ts b/frontend-react/e2e/pages/BasePage.ts index 28bf4641b2d..69a5441c8e6 100644 --- a/frontend-react/e2e/pages/BasePage.ts +++ b/frontend-react/e2e/pages/BasePage.ts @@ -1,9 +1,9 @@ -import { Locator, Page, Request, Route } from "@playwright/test"; +import { selectTestOrg } from "../helpers/utils"; import appInsightsConfig from "../mocks/appInsightsConfig.json" assert { type: "json" }; -import { TestArgs } from "../test"; +import { Locator, Page, Request, Response, Route, TestArgs } from "../test"; export type RouteHandlers = Record[1]>; -export type MockRouteCache = Record; +export type MockRouteCache = Record; export type GotoOptions = Parameters[1]; export interface BasePageProps { @@ -16,15 +16,22 @@ export type RouteFulfillOptions = Exclude< Parameters[0], undefined > & { isMock?: boolean }; -export type RouteFulfillOptionsFn = (request: Request) => RouteFulfillOptions; +export type RouteFulfillOptionsFn = ( + request: Request, +) => Promise | RouteFulfillOptions; export type RouteHandlerFn = (route: Route, request: Request) => Promise; export type RouteHandlerFulfillOptions = | RouteFulfillOptions | RouteFulfillOptionsFn; -export type RouteHandlerEntry = [ +export type RouteHandlerFulfillEntry = [ url: string, fulfillOptions: RouteHandlerFulfillOptions, ]; +export type ResponseHandlerEntry = [ + url: string, + handler: (response: Response) => Promise | void, +]; +export type RouteHandlerEntry = [url: string, handler: RouteHandlerFn]; export interface GotoRouteHandlerOptions { mock?: RouteHandlers; @@ -32,14 +39,31 @@ export interface GotoRouteHandlerOptions { mockError?: boolean | RouteFulfillOptions; } -export type BasePageTestArgs = TestArgs<"page" | "storageState">; +export type BasePageTestArgs = TestArgs<"page" | "storageState"> & { + isTestOrg?: boolean; +}; export abstract class BasePage { readonly page: Page; readonly url: string; readonly title: string; readonly testArgs: BasePageTestArgs; - readonly routeHandlers: Map[1]>; + /** + * Regular network routes with no special treatment in regards + * to mocking. + */ + readonly routeHandlers: Map; + /** + * Mock network routes that will only be used when mocking + * is allowed. + */ + readonly mockRouteHandlers: Map; + /** + * Handlers for network responses in FIFO order. WARNING: incorrect + * ordering or additions of handlers for requests that never fire + * will stall tests! + */ + readonly responseHandlers: ResponseHandlerEntry[]; protected _mockError?: RouteHandlerFulfillOptions; protected _mockRouteCache: MockRouteCache; @@ -54,7 +78,9 @@ export abstract class BasePage { this.url = url; this.title = title; this.testArgs = testArgs; - this.routeHandlers = new Map(); + this.routeHandlers = new Map(this.createDefaultRouteHandlers()); + this.mockRouteHandlers = new Map(); + this.responseHandlers = []; this._mockRouteCache = {}; this.heading = heading ?? this.page.locator("INVALID"); this.footer = this.page.locator("footer"); @@ -84,86 +110,162 @@ export abstract class BasePage { }; } - get isErrorExpected() { - return !!this.mockError; + /** + * Override this method as needed to ensure tests do not hang waiting + * for responses to network requests that will never occur. + */ + get isPageLoadExpected() { + return !this.mockError; + } + + get isAdminSession() { + return this.testArgs.storageState === "e2e/.auth/admin.json"; } + /** + * Reloads the page. + * Useful when setting a network error in a test. + */ async reload() { + if (this.isAdminSession && this.testArgs.isTestOrg) { + await this.selectTestOrg(); + } + await this.route(); - return await this.page.reload(); + return await this.handlePageLoad(this.page.reload()); } async goto(opts?: GotoOptions) { + if (this.isAdminSession && this.testArgs.isTestOrg) { + await this.selectTestOrg(); + } + await this.route(); - return await this.page.goto(this.url, { - waitUntil: "domcontentloaded", - ...opts, - }); + return await this.handlePageLoad( + this.page.goto(this.url, { + waitUntil: "domcontentloaded", + ...opts, + }), + ); + } + + /** + * Used to select the test org if logged-in user is Admin and the isTestOrg prop is set to true. + * This is needed for smoke tests since they use live data. + */ + async selectTestOrg() { + await selectTestOrg(this.page); + } + + async handlePageLoad(resP: Promise) { + if (this.isPageLoadExpected) { + await this.lifecycle_pageLoad(); + await this.handleNetworkResponses(); + } + + return await resP; + } + + async handleNetworkResponses() { + for (const [url, handler] of this.responseHandlers) { + await handler(await this.page.waitForResponse(url)); + } + } + + lifecycle_Route(): Promise | void { + return undefined as void; } async route() { - for (const [url, handler] of this.routeHandlers.entries()) { + await this.lifecycle_Route(); + const map = new Map([ + ...this.routeHandlers.entries(), + // Ignore our mockRouteHandlers if mocking is disabled + ...(this.isMocked ? this.mockRouteHandlers.entries() : []), + ]); + + for (const [url, handler] of map) { await this.page.route(url, handler); } } - addRouteHandlers(items: RouteHandlerEntry[]) { - for (const [url, _fulfillOptions] of items) { - const handler: RouteHandlerFn = async (route, request) => { - const { isMock, ...fulfillOptions } = + lifecycle_pageLoad(): void | Promise { + return undefined as void; + } + + /** + * Helper function to push onto responseHandlers array. + */ + addResponseHandlers(items: ResponseHandlerEntry[]) { + this.responseHandlers.push(...items); + } + + /** + * Wraps route fulfill option objects or functions with additional logic for + * mock error overrides and caching and adds to the mock route handler map. + */ + addMockRouteHandlers(items: RouteHandlerFulfillEntry[]) { + const wrapped = items.map(([url, _fulfillOptions]) => { + const fn = async (request: Request) => { + const fulfillOptions = typeof _fulfillOptions === "function" - ? _fulfillOptions(request) + ? await _fulfillOptions(request) : _fulfillOptions; - const mockErrorFulfillOptions = isMock - ? typeof this.mockError === "function" - ? this.mockError(request) - : this.mockError - : undefined; - const mockCacheFulfillOptions = isMock - ? await this.getMockCacheFulfillOptions(url) - : undefined; + const mockErrorFulfillOptions = + typeof this.mockError === "function" + ? await this.mockError(request) + : this.mockError; + const mockCacheFulfillOptions = this.getMockCacheFulfillOptions( + url, + fulfillOptions, + ); const mockOverrideFulfillOptions = mockErrorFulfillOptions ?? mockCacheFulfillOptions; - if (isMock && !this.isMocked) - throw new Error("Mocks are disabled"); - - return await route.fulfill( - mockOverrideFulfillOptions ?? fulfillOptions, - ); + return { + isMock: true, + ...mockOverrideFulfillOptions, + }; }; - this.routeHandlers.set(url, handler); - } + return [url, fn] as [url: string, fn: typeof fn]; + }); + + wrapped.forEach(([url, fn]) => + this.mockRouteHandlers.set(url, async (route, req) => + route.fulfill(await fn(req)), + ), + ); + + return wrapped; } - addMockRouteHandlers(items: RouteHandlerEntry[]) { - return this.addRouteHandlers( - items.map(([url, _fulfillOptions]) => { - if (typeof _fulfillOptions === "object") { - return [ - url, - { - isMock: true, - ..._fulfillOptions, - }, - ]; - } + /** + * Helper function to convert RouteHandlerFulfillEntries to RouteHandlerEntries. + */ + createRouteHandlers( + items: RouteHandlerFulfillEntry[], + ): RouteHandlerEntry[] { + return items.map(([url, _fulfill]) => { + const handler = async (route: Route, request: Request) => { + const fulfill = + typeof _fulfill === "function" + ? await _fulfill(request) + : _fulfill; - const fn: RouteFulfillOptionsFn = (request) => { - return { - isMock: true, - ..._fulfillOptions(request), - }; - }; - return [url, fn]; - }), - ); + return route.fulfill(fulfill); + }; + + return [url, handler]; + }); } - addDefaultRouteHandlers() { - return this.addRouteHandlers([ + /** + * Add misc network requests that we want to prevent for tests here (NOT API MOCKS). + */ + createDefaultRouteHandlers(): RouteHandlerEntry[] { + return this.createRouteHandlers([ // Azure Application Insights Tracking [ "*.in.applicationinsights.azure.com/v2/track", @@ -196,15 +298,20 @@ export abstract class BasePage { ]); } - async getMockCacheFulfillOptions(url: string) { + /** + * Get or warm the cache for a particular mock URL's fulfillOptions. This + * allows for dynamic options to persist across page reloads for consistency. + */ + getMockCacheFulfillOptions( + url: string, + fulfillOptions: RouteFulfillOptions, + ) { const cache = this._mockRouteCache[url]; - if (!cache) return cache; - - return { - body: await cache.text(), - headers: Object.fromEntries(cache.headers.entries()), - path: cache.url, - status: cache.status, - }; + if (!cache) { + this._mockRouteCache[url] = fulfillOptions; + return fulfillOptions; + } + + return cache; } } diff --git a/frontend-react/e2e/pages/about-side-navigation.ts b/frontend-react/e2e/pages/about-side-navigation.ts index e4a54239c45..cd685dbad0e 100644 --- a/frontend-react/e2e/pages/about-side-navigation.ts +++ b/frontend-react/e2e/pages/about-side-navigation.ts @@ -15,10 +15,7 @@ export async function clickRoadmap(page: Page) { } export async function clickNews(page: Page) { - await page - .getByTestId("sidenav") - .getByRole("link", { name: /News/ }) - .click(); + await page.getByTestId("sidenav").getByRole("link", { name: /News/ }).click(); } export async function clickCaseStudies(page: Page) { diff --git a/frontend-react/e2e/pages/about.ts b/frontend-react/e2e/pages/about.ts index a684ac229e6..2189c703f39 100644 --- a/frontend-react/e2e/pages/about.ts +++ b/frontend-react/e2e/pages/about.ts @@ -5,7 +5,7 @@ export class AboutPage extends BasePage { super( { url: "/about", - title: "About", + title: "About ReportStream", heading: testArgs.page.getByRole("heading", { name: "About", }), diff --git a/frontend-react/e2e/pages/admin/receiver-status.ts b/frontend-react/e2e/pages/admin/receiver-status.ts index cb6982af271..0f69f4bb691 100644 --- a/frontend-react/e2e/pages/admin/receiver-status.ts +++ b/frontend-react/e2e/pages/admin/receiver-status.ts @@ -1,4 +1,4 @@ -import { expect, Locator, Response } from "@playwright/test"; +import { expect, Locator } from "@playwright/test"; import { endOfDay, format, startOfDay, subDays } from "date-fns"; import { RSReceiverStatus } from "../../../src/hooks/api/UseReceiversConnectionStatus/UseReceiversConnectionStatus"; import { @@ -10,8 +10,8 @@ import { createMockGetReceiverStatus } from "../../mocks/receiverStatus"; import { BasePage, BasePageTestArgs, - GotoOptions, - RouteHandlerEntry, + type ResponseHandlerEntry, + type RouteHandlerFulfillEntry, } from "../BasePage"; export interface AdminReceiverStatusPageUpdateFiltersProps { @@ -89,6 +89,11 @@ export class AdminReceiverStatusPage extends BasePage { testArgs, ); + this.addMockRouteHandlers([this.createMockReceiverStatusHandler()]); + this.addResponseHandlers([ + this.createMockReceiverStatusResponseHandler(), + ]); + const now = new Date(); this._receiverStatus = []; @@ -186,10 +191,10 @@ export class AdminReceiverStatusPage extends BasePage { /** * Error expected additionally if user context isn't admin */ - get isErrorExpected() { + get isPageLoadExpected() { return ( - super.isErrorExpected || - this.testArgs.storageState !== this.testArgs.adminLogin.path + super.isPageLoadExpected && + this.testArgs.storageState === this.testArgs.adminLogin.path ); } @@ -201,46 +206,7 @@ export class AdminReceiverStatusPage extends BasePage { return this._timePeriodData; } - async handlePageLoad(res: Response | null) { - if (this.isErrorExpected) return res; - - const apiRes = await this.page.waitForResponse( - AdminReceiverStatusPage.API_RECEIVER_STATUS, - ); - - const data: RSReceiverStatus[] = await apiRes.json(); - const url = new URL(apiRes.url()); - const startDate = url.searchParams.get("start_date"); - const endDate = url.searchParams.get("end_date"); - const range = - startDate && endDate - ? ([new Date(startDate), new Date(endDate)] as DatePair) - : undefined; - this._receiverStatus = data; - this._timePeriodData = range - ? this.createTimePeriodData({ data, range }) - : []; - - return res; - } - - async reload() { - if (this.isMocked) { - this.addMockRouteHandlers([this.createMockReceiverStatusHandler()]); - } - - return await this.handlePageLoad(await super.reload()); - } - - async goto(opts?: GotoOptions) { - if (this.isMocked) { - this.addMockRouteHandlers([this.createMockReceiverStatusHandler()]); - } - - return await this.handlePageLoad(await super.goto(opts)); - } - - createMockReceiverStatusHandler(): RouteHandlerEntry { + createMockReceiverStatusHandler(): RouteHandlerFulfillEntry { return [ AdminReceiverStatusPage.API_RECEIVER_STATUS, (request) => { @@ -259,6 +225,26 @@ export class AdminReceiverStatusPage extends BasePage { ]; } + createMockReceiverStatusResponseHandler(): ResponseHandlerEntry { + return [ + AdminReceiverStatusPage.API_RECEIVER_STATUS, + async (apiRes) => { + const data: RSReceiverStatus[] = await apiRes.json(); + const url = new URL(apiRes.url()); + const startDate = url.searchParams.get("start_date"); + const endDate = url.searchParams.get("end_date"); + const range = + startDate && endDate + ? ([new Date(startDate), new Date(endDate)] as DatePair) + : undefined; + this._receiverStatus = data; + this._timePeriodData = range + ? this.createTimePeriodData({ data, range }) + : []; + }, + ]; + } + createMockReceiverStatuses( ...args: Parameters ) { diff --git a/frontend-react/e2e/pages/daily-data-details.ts b/frontend-react/e2e/pages/daily-data-details.ts index bfafd23e7b8..5c7fabf535b 100644 --- a/frontend-react/e2e/pages/daily-data-details.ts +++ b/frontend-react/e2e/pages/daily-data-details.ts @@ -1,19 +1,87 @@ -import { expect, Page } from "@playwright/test"; +import { + BasePage, + BasePageTestArgs, + type RouteHandlerFulfillEntry, +} from "./BasePage"; +import { API_WATERS_REPORT, URL_REPORT_DETAILS } from "./report-details"; +import { RSDelivery, RSFacility } from "../../src/config/endpoints/deliveries"; +import { MOCK_GET_DELIVERY } from "../mocks/delivery"; +import { MOCK_GET_FACILITIES } from "../mocks/facilities"; -export async function title(page: Page) { - await expect(page).toHaveTitle( - /ReportStream - CDC's free, interoperable data transfer platform/, - ); -} +const id = "73e3cbc8-9920-4ab7-871f-843a1db4c074"; + +export class DailyDataDetailsPage extends BasePage { + static readonly API_DELIVERY = `${API_WATERS_REPORT}/${id}/delivery`; + static readonly API_FACILITIES = `${API_WATERS_REPORT}/${id}/facilities`; + protected _reportDelivery: RSDelivery; + protected _facilities: RSFacility[]; + + constructor(testArgs: BasePageTestArgs) { + super( + { + url: `${URL_REPORT_DETAILS}/${id}`, + title: "Daily Data - ReportStream", + heading: testArgs.page.getByRole("heading", { + name: "Daily Data Details", + }), + }, + testArgs, + ); + + this._reportDelivery = { + batchReadyAt: "", + deliveryId: 0, + expires: "", + fileName: "", + fileType: "", + receiver: "", + reportId: "", + reportItemCount: 0, + topic: "", + }; + this._facilities = []; + this.addResponseHandlers([ + [ + DailyDataDetailsPage.API_DELIVERY, + async (res) => (this._reportDelivery = await res.json()), + ], + [ + DailyDataDetailsPage.API_FACILITIES, + async (res) => (this._facilities = await res.json()), + ], + ]); + this.addMockRouteHandlers([ + this.createMockDeliveryHandler(), + this.createMockFacilitiesHandler(), + ]); + } + + get isPageLoadExpected() { + return ( + super.isPageLoadExpected && + this.testArgs.storageState === this.testArgs.adminLogin.path + ); + } + + createMockDeliveryHandler(): RouteHandlerFulfillEntry { + return [ + DailyDataDetailsPage.API_DELIVERY, + () => { + return { + json: MOCK_GET_DELIVERY, + }; + }, + ]; + } -export async function tableHeaders(page: Page) { - await expect(page.locator(".usa-table th").nth(0)).toHaveText(/Facility/); - await expect(page.locator(".usa-table th").nth(1)).toHaveText(/Location/); - await expect(page.locator(".usa-table th").nth(2)).toHaveText(/CLIA/); - await expect(page.locator(".usa-table th").nth(3)).toHaveText( - /Total tests/, - ); - await expect(page.locator(".usa-table th").nth(4)).toHaveText( - /Total positive/, - ); + createMockFacilitiesHandler(): RouteHandlerFulfillEntry { + return [ + DailyDataDetailsPage.API_FACILITIES, + () => { + return { + json: MOCK_GET_FACILITIES, + }; + }, + ]; + } } diff --git a/frontend-react/e2e/pages/daily-data.ts b/frontend-react/e2e/pages/daily-data.ts index 7f79c567693..dd448e49946 100644 --- a/frontend-react/e2e/pages/daily-data.ts +++ b/frontend-react/e2e/pages/daily-data.ts @@ -1,7 +1,12 @@ import { expect, Page } from "@playwright/test"; import { format } from "date-fns"; +import { + MOCK_GET_RECEIVERS_AK, + MOCK_GET_RECEIVERS_IGNORE, +} from "../mocks/organizations"; const URL_DAILY_DATA = "/daily-data"; +const API_ORGANIZATIONS = "**/api/settings/organizations"; export async function goto(page: Page) { await page.goto(URL_DAILY_DATA, { @@ -156,3 +161,23 @@ export function filterStatus(page: Page, filters: (string | undefined)[]) { } return filterStatus; } + +export async function mockGetOrgAlaskaReceiversResponse( + page: Page, + responseStatus = 200, +) { + await page.route(`${API_ORGANIZATIONS}/ak-phd/receivers`, async (route) => { + const json = MOCK_GET_RECEIVERS_AK; + await route.fulfill({ json, status: responseStatus }); + }); +} + +export async function mockGetOrgIgnoreReceiversResponse( + page: Page, + responseStatus = 200, +) { + await page.route(`${API_ORGANIZATIONS}/ignore/receivers`, async (route) => { + const json = MOCK_GET_RECEIVERS_IGNORE; + await route.fulfill({ json, status: responseStatus }); + }); +} diff --git a/frontend-react/e2e/pages/managing-your-connection.ts b/frontend-react/e2e/pages/managing-your-connection.ts index 84cbcf63660..6098bec9e63 100644 --- a/frontend-react/e2e/pages/managing-your-connection.ts +++ b/frontend-react/e2e/pages/managing-your-connection.ts @@ -1,6 +1,22 @@ import { expect, Page } from "@playwright/test"; +import { BasePage, BasePageTestArgs } from "./BasePage"; export async function onLoad(page: Page) { await expect(page).toHaveURL(/managing-your-connection/); await expect(page).toHaveTitle(/Managing your connection/); } + +export class ManagingYourConnectionPage extends BasePage { + constructor(testArgs: BasePageTestArgs) { + super( + { + url: "/managing-your-connection", + title: "Managing your connection with ReportStream", + heading: testArgs.page.getByRole("heading", { + name: "Managing Your Connection", + }), + }, + testArgs, + ); + } +} diff --git a/frontend-react/e2e/pages/organization.ts b/frontend-react/e2e/pages/organization.ts index b1cf4264d21..bbac970dd64 100644 --- a/frontend-react/e2e/pages/organization.ts +++ b/frontend-react/e2e/pages/organization.ts @@ -1,33 +1,51 @@ -import { Page } from "@playwright/test"; - import { - MOCK_GET_RECEIVERS_AK, - MOCK_GET_RECEIVERS_IGNORE, -} from "../mocks/organizations"; + BasePage, + BasePageTestArgs, + type RouteHandlerFulfillEntry, +} from "./BasePage"; +import { RSOrganizationSettings } from "../../src/config/endpoints/settings"; +import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../mocks/organizations"; -export const API_ORGANIZATIONS = "**/api/settings/organizations"; -export async function goto(page: Page) { - await page.goto("/admin/settings", { - waitUntil: "domcontentloaded", - }); -} +export class OrganizationPage extends BasePage { + static readonly API_ORGANIZATIONS = "/api/settings/organizations"; + protected _organizationSettings: RSOrganizationSettings[]; + constructor(testArgs: BasePageTestArgs) { + super( + { + url: "/admin/settings", + title: "Organizations", + heading: testArgs.page.getByRole("heading", { + name: "Organizations", + }), + }, + testArgs, + ); -export async function mockGetOrgAlaskaReceiversResponse( - page: Page, - responseStatus = 200, -) { - await page.route(`${API_ORGANIZATIONS}/ak-phd/receivers`, async (route) => { - const json = MOCK_GET_RECEIVERS_AK; - await route.fulfill({ json, status: responseStatus }); - }); -} + this._organizationSettings = []; + this.addResponseHandlers([ + [ + OrganizationPage.API_ORGANIZATIONS, + async (res) => (this._organizationSettings = await res.json()), + ], + ]); + this.addMockRouteHandlers([this.createMockOrganizationHandler()]); + } + + get isPageLoadExpected() { + return ( + super.isPageLoadExpected && + this.testArgs.storageState === this.testArgs.adminLogin.path + ); + } -export async function mockGetOrgIgnoreReceiversResponse( - page: Page, - responseStatus = 200, -) { - await page.route(`${API_ORGANIZATIONS}/ignore/receivers`, async (route) => { - const json = MOCK_GET_RECEIVERS_IGNORE; - await route.fulfill({ json, status: responseStatus }); - }); + createMockOrganizationHandler(): RouteHandlerFulfillEntry { + return [ + OrganizationPage.API_ORGANIZATIONS, + () => { + return { + json: MOCK_GET_ORGANIZATION_SETTINGS_LIST, + }; + }, + ]; + } } diff --git a/frontend-react/e2e/pages/refer-healthcare.ts b/frontend-react/e2e/pages/refer-healthcare.ts new file mode 100644 index 00000000000..b86152b914f --- /dev/null +++ b/frontend-react/e2e/pages/refer-healthcare.ts @@ -0,0 +1,16 @@ +import { BasePage, BasePageTestArgs } from "./BasePage"; + +export class ReferHealthcarePage extends BasePage { + constructor(testArgs: BasePageTestArgs) { + super( + { + url: "/managing-your-connection/refer-healthcare-organizations", + title: "Refer health care organizations to ReportStream", + heading: testArgs.page.getByRole("heading", { + name: "Refer healthcare organizations", + }), + }, + testArgs, + ); + } +} diff --git a/frontend-react/e2e/pages/support.ts b/frontend-react/e2e/pages/support.ts index 3524040db16..ae5bc223708 100644 --- a/frontend-react/e2e/pages/support.ts +++ b/frontend-react/e2e/pages/support.ts @@ -8,6 +8,7 @@ export class SupportPage extends BasePage { title: "ReportStream support", heading: testArgs.page.getByRole("heading", { name: "Support", + exact: true, }), }, testArgs, diff --git a/frontend-react/e2e/spec/all/BasePage.spec.ts b/frontend-react/e2e/spec/all/BasePage.spec.ts new file mode 100644 index 00000000000..c0a688c2508 --- /dev/null +++ b/frontend-react/e2e/spec/all/BasePage.spec.ts @@ -0,0 +1,104 @@ +import { BasePage, type BasePageTestArgs } from "../../pages/BasePage"; +import { test as baseTest, expect } from "../../test"; + +export interface MockPageFixtures { + mockPage: MockPage; +} + +class MockPage extends BasePage { + data?: any; + + constructor(testArgs: BasePageTestArgs) { + super( + { + url: "/", + title: "", + }, + testArgs, + ); + this.addResponseHandlers([ + [ + "fake", + async (res) => { + this.data = await res.json(); + }, + ], + ]); + } + + lifecycle_Route() { + if (this.isMocked) { + this.addMockRouteHandlers([["/fake", { json: { foo: "bar" } }]]); + } else { + const [url, handler] = this.createRouteHandlers([ + ["/fake", { json: { bar: "foo" } }], + ])[0]; + this.routeHandlers.set(url, handler); + } + + const [url, handler] = this.createRouteHandlers([ + [ + "/", + { + body: "

Fake

", + contentType: "text/html", + }, + ], + ])[0]; + + this.routeHandlers.set(url, handler); + } +} + +const test = baseTest.extend({ + mockPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new MockPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe("mocking", () => { + test.describe("enabled", () => { + test.use({ isMockDisabled: false }); + + test("renders", async ({ mockPage }) => { + await expect(mockPage.page.getByText("Fake")).toBeVisible(); + }); + test("returns mocked data", ({ mockPage }) => { + expect(mockPage.data).toEqual({ foo: "bar" }); + }); + }); + + test.describe("mocking disabled", () => { + test.use({ isMockDisabled: true }); + + test("renders", async ({ mockPage }) => { + await expect(mockPage.page.getByText("Fake")).toBeVisible(); + }); + test("returns real data", ({ mockPage }) => { + expect(mockPage.data).toEqual({ bar: "foo" }); + }); + }); +}); diff --git a/frontend-react/e2e/spec/all/about-page.spec.ts b/frontend-react/e2e/spec/all/about-page.spec.ts index 34090f13914..11c8b2e8597 100644 --- a/frontend-react/e2e/spec/all/about-page.spec.ts +++ b/frontend-react/e2e/spec/all/about-page.spec.ts @@ -58,7 +58,8 @@ test.describe("About page", () => { }); test("has correct title", async ({ aboutPage }) => { - await expect(aboutPage.page).toHaveTitle(/About/); + await expect(aboutPage.page).toHaveTitle(aboutPage.title); + await expect(aboutPage.heading).toBeVisible(); }); test.describe("In this section", () => { diff --git a/frontend-react/e2e/spec/all/admin/receiver-status-page.spec.ts b/frontend-react/e2e/spec/all/admin/receiver-status-page.spec.ts index ddf070c48f9..1855e011306 100644 --- a/frontend-react/e2e/spec/all/admin/receiver-status-page.spec.ts +++ b/frontend-react/e2e/spec/all/admin/receiver-status-page.spec.ts @@ -33,6 +33,7 @@ const test = baseTest.extend({ storageState, isFrontendWarningsLog, frontendWarningsLogPath, + isTestOrg: true, }); await page.goto(); await use(page); @@ -78,178 +79,237 @@ test.describe("Admin Receiver Status Page", () => { ).toBeVisible(); }); - test("Has correct title", async ({ adminReceiverStatusPage }) => { - await expect(adminReceiverStatusPage.page).toHaveURL( - adminReceiverStatusPage.url, - ); - await expect(adminReceiverStatusPage.page).toHaveTitle( - adminReceiverStatusPage.title, - ); - }); + test( + "Has correct title", + { + tag: "@smoke", + }, + async ({ adminReceiverStatusPage }) => { + await expect(adminReceiverStatusPage.page).toHaveURL( + adminReceiverStatusPage.url, + ); + await expect(adminReceiverStatusPage.page).toHaveTitle( + adminReceiverStatusPage.title, + ); + }, + ); test.describe("When there is no error", () => { - test.describe("Displays correctly", () => { - test("header", async ({ adminReceiverStatusPage }) => { - await expect(adminReceiverStatusPage.heading).toBeVisible(); - }); - - test.describe("filters", () => { - test("date range", async ({ adminReceiverStatusPage }) => { - const { button, label, modalOverlay, valueDisplay } = - adminReceiverStatusPage.filterFormInputs.dateRange; - await expect(label).toBeVisible(); - await expect(button).toBeVisible(); - await expect(valueDisplay).toHaveText( - adminReceiverStatusPage.expectedDateRangeLabelText, - ); - await expect(modalOverlay).toBeHidden(); - }); - test("receiver name", async ({ - adminReceiverStatusPage, - }) => { - const { - input, - expectedTooltipText, - label, - tooltip, - expectedDefaultValue, - } = - adminReceiverStatusPage.filterFormInputs - .receiverName; - await expect(label).toBeVisible(); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(expectedDefaultValue); - - await expect(tooltip).toBeHidden(); - await input.hover(); - await expect(tooltip).toBeVisible(); - await expect(tooltip).toHaveText(expectedTooltipText); - }); - test("results message", async ({ - adminReceiverStatusPage, - }) => { - const { - input, - expectedTooltipText, - label, - tooltip, - expectedDefaultValue, - } = - adminReceiverStatusPage.filterFormInputs - .resultMessage; - await expect(label).toBeVisible(); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(expectedDefaultValue); - - await expect(tooltip).toBeHidden(); - await input.hover(); - await expect(tooltip).toBeVisible(); - await expect(tooltip).toHaveText(expectedTooltipText); - }); - test("success type", async ({ - adminReceiverStatusPage, - }) => { - const { - input, - expectedTooltipText, - label, - tooltip, - expectedDefaultValue, - } = - adminReceiverStatusPage.filterFormInputs - .successType; - await expect(label).toBeVisible(); - await expect(input).toBeVisible(); - await expect(input).toHaveValue(expectedDefaultValue); - - await expect(tooltip).toBeHidden(); - await input.hover(); - await expect(tooltip).toBeVisible(); - await expect(tooltip).toHaveText(expectedTooltipText); + test.describe( + "Displays correctly", + { + tag: "@smoke", + }, + () => { + test("header", async ({ adminReceiverStatusPage }) => { + await expect( + adminReceiverStatusPage.heading, + ).toBeVisible(); }); - }); - // Failures here indicate potential misalignment of playwright/browser timezone - test.describe("receiver statuses", () => { - test("time periods", async ({ - adminReceiverStatusPage, - }) => { - const result = - await adminReceiverStatusPage.testReceiverStatusDisplay(); - expect(result).toBe(true); - }); - }); - }); + test.describe( + "filters", + { + tag: "@smoke", + }, + () => { + test("date range", async ({ + adminReceiverStatusPage, + }) => { + const { + button, + label, + modalOverlay, + valueDisplay, + } = + adminReceiverStatusPage.filterFormInputs + .dateRange; + await expect(label).toBeVisible(); + await expect(button).toBeVisible(); + await expect(valueDisplay).toHaveText( + adminReceiverStatusPage.expectedDateRangeLabelText, + ); + await expect(modalOverlay).toBeHidden(); + }); - test.describe("Functions correctly", () => { - test.describe("filters", () => { - test.describe("date range", () => { - test("works through calendar", async ({ - adminReceiverStatusPage, - }) => { - const { valueDisplay } = - adminReceiverStatusPage.filterFormInputs - .dateRange; - const now = new Date(); - const targetFrom = startOfDay(subDays(now, 3)); - const targetTo = addDays(endOfDay(now), 1); + test("receiver name", async ({ + adminReceiverStatusPage, + }) => { + const { + input, + expectedTooltipText, + label, + tooltip, + expectedDefaultValue, + } = + adminReceiverStatusPage.filterFormInputs + .receiverName; + await expect(label).toBeVisible(); + await expect(input).toBeVisible(); + await expect(input).toHaveValue( + expectedDefaultValue, + ); - const reqUrl = - await adminReceiverStatusPage.updateFilters({ - dateRange: { - value: [targetFrom, targetTo], - }, - }); - expect(reqUrl).toBeDefined(); + await expect(tooltip).toBeHidden(); + await input.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText( + expectedTooltipText, + ); + }); - await expect(valueDisplay).toHaveText( - adminReceiverStatusPage.expectedDateRangeLabelText, - ); - expect( - Object.fromEntries( - reqUrl!.searchParams.entries(), - ), - ).toMatchObject({ - start_date: targetFrom.toISOString(), - end_date: targetTo.toISOString(), + test("results message", async ({ + adminReceiverStatusPage, + }) => { + const { + input, + expectedTooltipText, + label, + tooltip, + expectedDefaultValue, + } = + adminReceiverStatusPage.filterFormInputs + .resultMessage; + await expect(label).toBeVisible(); + await expect(input).toBeVisible(); + await expect(input).toHaveValue( + expectedDefaultValue, + ); + + await expect(tooltip).toBeHidden(); + await input.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText( + expectedTooltipText, + ); }); - }); - test("works through textboxes", async ({ - adminReceiverStatusPage, - }) => { - const { valueDisplay } = - adminReceiverStatusPage.filterFormInputs - .dateRange; - await expect( - adminReceiverStatusPage.receiverStatusRowsLocator, - ).not.toHaveCount(0); - const now = new Date(); - const targetFrom = startOfDay(subDays(now, 3)); - const targetTo = addDays(endOfDay(now), 1); + test("success type", async ({ + adminReceiverStatusPage, + }) => { + const { + input, + expectedTooltipText, + label, + tooltip, + expectedDefaultValue, + } = + adminReceiverStatusPage.filterFormInputs + .successType; + await expect(label).toBeVisible(); + await expect(input).toBeVisible(); + await expect(input).toHaveValue( + expectedDefaultValue, + ); - const reqUrl = - await adminReceiverStatusPage.updateFilters({ - dateRange: { - value: [targetFrom, targetTo], - }, + await expect(tooltip).toBeHidden(); + await input.hover(); + await expect(tooltip).toBeVisible(); + await expect(tooltip).toHaveText( + expectedTooltipText, + ); + }); + }, + ); + + // Failures here indicate potential misalignment of playwright/browser timezone + test.describe( + "receiver statuses", + { + tag: "@smoke", + }, + () => { + test("time periods", async ({ + adminReceiverStatusPage, + }) => { + const result = + await adminReceiverStatusPage.testReceiverStatusDisplay(); + expect(result).toBe(true); + }); + }, + ); + }, + ); + + test.describe("Functions correctly", () => { + test.describe("filters", () => { + test.describe( + "date range", + { + tag: "@smoke", + }, + () => { + test("works through calendar", async ({ + adminReceiverStatusPage, + }) => { + const { valueDisplay } = + adminReceiverStatusPage.filterFormInputs + .dateRange; + const now = new Date(); + const targetFrom = startOfDay(subDays(now, 3)); + const targetTo = addDays(endOfDay(now), 1); + + const reqUrl = + await adminReceiverStatusPage.updateFilters( + { + dateRange: { + value: [targetFrom, targetTo], + }, + }, + ); + expect(reqUrl).toBeDefined(); + + await expect(valueDisplay).toHaveText( + adminReceiverStatusPage.expectedDateRangeLabelText, + ); + expect( + Object.fromEntries( + reqUrl!.searchParams.entries(), + ), + ).toMatchObject({ + start_date: targetFrom.toISOString(), + end_date: targetTo.toISOString(), }); + }); - expect(reqUrl).toBeDefined(); + test("works through textboxes", async ({ + adminReceiverStatusPage, + }) => { + const { valueDisplay } = + adminReceiverStatusPage.filterFormInputs + .dateRange; + await expect( + adminReceiverStatusPage.receiverStatusRowsLocator, + ).not.toHaveCount(0); + const now = new Date(); + const targetFrom = startOfDay(subDays(now, 3)); + const targetTo = addDays(endOfDay(now), 1); + + const reqUrl = + await adminReceiverStatusPage.updateFilters( + { + dateRange: { + value: [targetFrom, targetTo], + }, + }, + ); - await expect(valueDisplay).toHaveText( - adminReceiverStatusPage.expectedDateRangeLabelText, - ); - expect( - Object.fromEntries( - reqUrl!.searchParams.entries(), - ), - ).toMatchObject({ - start_date: targetFrom.toISOString(), - end_date: targetTo.toISOString(), + expect(reqUrl).toBeDefined(); + + await expect(valueDisplay).toHaveText( + adminReceiverStatusPage.expectedDateRangeLabelText, + ); + expect( + Object.fromEntries( + reqUrl!.searchParams.entries(), + ), + ).toMatchObject({ + start_date: targetFrom.toISOString(), + end_date: targetTo.toISOString(), + }); }); - }); - }); + }, + ); test("receiver name", async ({ adminReceiverStatusPage, @@ -406,7 +466,9 @@ test.describe("Admin Receiver Status Page", () => { const targetFrom = startOfDay(subDays(now, 3)); const targetTo = endOfDay(now); await adminReceiverStatusPage.updateFilters({ - dateRange: { value: [targetFrom, targetTo] }, + dateRange: { + value: [targetFrom, targetTo], + }, }); await expect(days).toHaveCount(4); }); @@ -422,7 +484,9 @@ test.describe("Admin Receiver Status Page", () => { const targetFrom = startOfDay(subDays(now, 1)); const targetTo = endOfDay(now); await adminReceiverStatusPage.updateFilters({ - dateRange: { value: [targetFrom, targetTo] }, + dateRange: { + value: [targetFrom, targetTo], + }, }); await expect(days).toHaveCount(2); }); diff --git a/frontend-react/e2e/spec/all/daily-data-details-page.spec.ts b/frontend-react/e2e/spec/all/daily-data-details-page.spec.ts index 30188d1b3aa..fb9c12fd312 100644 --- a/frontend-react/e2e/spec/all/daily-data-details-page.spec.ts +++ b/frontend-react/e2e/spec/all/daily-data-details-page.spec.ts @@ -1,15 +1,50 @@ -import { expect, test } from "@playwright/test"; -import { selectTestOrg, tableDataCellValue } from "../../helpers/utils"; +import { expect } from "@playwright/test"; +import { tableDataCellValue } from "../../helpers/utils"; import { detailsTableHeaders, title } from "../../pages/daily-data"; +import { DailyDataDetailsPage } from "../../pages/daily-data-details"; import * as reportDetails from "../../pages/report-details"; - +import { test as baseTest } from "../../test"; + +export interface DailyDataDetailsPageFixtures { + dailyDataDetailsPage: DailyDataDetailsPage; +} + +const test = baseTest.extend({ + dailyDataDetailsPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new DailyDataDetailsPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + isTestOrg: true, + }); + await page.goto(); + await use(page); + }, +}); const id = "73e3cbc8-9920-4ab7-871f-843a1db4c074"; const fileName = `hhsprotect-covid-19-73e3cbc8-9920-4ab7-871f-843a1db4c074.csv`; + test.describe("Daily Data Details page", () => { test.describe("not authenticated", () => { - test("redirects to login", async ({ page }) => { - await reportDetails.goto(page, id); - await expect(page).toHaveURL("/login"); + test("redirects to login", async ({ dailyDataDetailsPage }) => { + await expect(dailyDataDetailsPage.page).toHaveURL("/login"); }); }); @@ -17,123 +52,174 @@ test.describe("Daily Data Details page", () => { test.use({ storageState: "e2e/.auth/admin.json" }); test.describe("without org selected", () => { - test.beforeEach(async ({ page }) => { - await reportDetails.mockGetReportDeliveryResponse(page, id); - await reportDetails.mockGetReportFacilitiesResponse(page, id); - await reportDetails.goto(page, id); - - await page.getByRole("table").waitFor({ state: "visible" }); - }); - - test("has correct title", async ({ page }) => { - await title(page); + test.beforeEach(async ({ dailyDataDetailsPage }) => { + await dailyDataDetailsPage.page + .getByRole("table") + .waitFor({ state: "visible" }); }); test.describe("table", () => { - test("has correct headers", async ({ page }) => { - await detailsTableHeaders(page); + test("has correct headers", async ({ + dailyDataDetailsPage, + }) => { + await detailsTableHeaders(dailyDataDetailsPage.page); }); test("'Facility' column has expected data", async ({ - page, + dailyDataDetailsPage, }) => { - expect(await tableDataCellValue(page, 0, 0)).toEqual( - "Any lab USA 1", - ); + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 0, + ), + ).toEqual("Any lab USA 1"); }); test("'Location' column has expected data", async ({ - page, + dailyDataDetailsPage, }) => { - expect(await tableDataCellValue(page, 0, 1)).toEqual( - "Juneau, AK", - ); + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 1, + ), + ).toEqual("Juneau, AK"); }); - test("'CLIA' column has expected data", async ({ page }) => { - expect(await tableDataCellValue(page, 0, 2)).toEqual( - "34D8574402", - ); + test("'CLIA' column has expected data", async ({ + dailyDataDetailsPage, + }) => { + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 2, + ), + ).toEqual("34D8574402"); }); test("'Total tests' column has expected data", async ({ - page, + dailyDataDetailsPage, }) => { - expect(await tableDataCellValue(page, 0, 3)).toEqual("10"); + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 3, + ), + ).toEqual("10"); }); test("'Total positive' column has expected data", async ({ - page, + dailyDataDetailsPage, }) => { - expect(await tableDataCellValue(page, 0, 4)).toEqual("1"); + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 4, + ), + ).toEqual("1"); }); }); - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); + test("has footer", async ({ dailyDataDetailsPage }) => { + await expect( + dailyDataDetailsPage.page.locator("footer"), + ).toBeAttached(); }); }); test.describe("with org selected", () => { - test.beforeEach(async ({ page }) => { - await selectTestOrg(page); - await reportDetails.mockGetReportDeliveryResponse(page, id); - await reportDetails.mockGetReportFacilitiesResponse(page, id); - await reportDetails.goto(page, id); - - await page.getByRole("table").waitFor({ state: "visible" }); - }); - - test("has correct title", async ({ page }) => { - await title(page); + test.beforeEach(async ({ dailyDataDetailsPage }) => { + await dailyDataDetailsPage.page + .getByRole("table") + .waitFor({ state: "visible" }); }); test.describe("table", () => { - test("has correct headers", async ({ page }) => { - await detailsTableHeaders(page); + test("has correct headers", async ({ + dailyDataDetailsPage, + }) => { + await detailsTableHeaders(dailyDataDetailsPage.page); }); test("'Facility' column has expected data", async ({ - page, + dailyDataDetailsPage, }) => { - expect(await tableDataCellValue(page, 0, 0)).toEqual( - "Any lab USA 1", - ); + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 0, + ), + ).toEqual("Any lab USA 1"); }); test("'Location' column has expected data", async ({ - page, + dailyDataDetailsPage, }) => { - expect(await tableDataCellValue(page, 0, 1)).toEqual( - "Juneau, AK", - ); + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 1, + ), + ).toEqual("Juneau, AK"); }); - test("'CLIA' column has expected data", async ({ page }) => { - expect(await tableDataCellValue(page, 0, 2)).toEqual( - "34D8574402", - ); + test("'CLIA' column has expected data", async ({ + dailyDataDetailsPage, + }) => { + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 2, + ), + ).toEqual("34D8574402"); }); test("'Total tests' column has expected data", async ({ - page, + dailyDataDetailsPage, }) => { - expect(await tableDataCellValue(page, 0, 3)).toEqual("10"); + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 3, + ), + ).toEqual("10"); }); test("'Total positive' column has expected data", async ({ - page, + dailyDataDetailsPage, }) => { - expect(await tableDataCellValue(page, 0, 4)).toEqual("1"); + expect( + await tableDataCellValue( + dailyDataDetailsPage.page, + 0, + 4, + ), + ).toEqual("1"); }); }); - test("should download file", async ({ page }) => { - await reportDetails.downloadFile(page, id, fileName); + test("should download file", async ({ dailyDataDetailsPage }) => { + await reportDetails.downloadFile( + dailyDataDetailsPage.page, + id, + fileName, + ); }); - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); + test("has footer", async ({ dailyDataDetailsPage }) => { + await expect( + dailyDataDetailsPage.page.locator("footer"), + ).toBeAttached(); }); }); }); @@ -141,123 +227,165 @@ test.describe("Daily Data Details page", () => { test.describe("admin user - server error", () => { test.use({ storageState: "e2e/.auth/admin.json" }); - test.beforeEach(async ({ page }) => { - await reportDetails.mockGetReportDeliveryResponse(page, id, 500); - await reportDetails.goto(page, id); + test.beforeEach(async ({ dailyDataDetailsPage }) => { + await reportDetails.mockGetReportDeliveryResponse( + dailyDataDetailsPage.page, + id, + 500, + ); + await reportDetails.goto(dailyDataDetailsPage.page, id); }); - test("has alert", async ({ page }) => { - await expect(page.getByTestId("alert")).toBeAttached(); + test("has alert", async ({ dailyDataDetailsPage }) => { + // TODO: Fix - mockError is undefined + //dailyDataDetailsPage.mockError = true; + // await dailyDataDetailsPage.reload(); await expect( - page.getByText( + dailyDataDetailsPage.page.getByTestId("alert"), + ).toBeAttached(); + await expect( + dailyDataDetailsPage.page.getByText( /Our apologies, there was an error loading this content./, ), - ).toBeAttached(); + ).toBeVisible(); }); - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); + test("has footer", async ({ dailyDataDetailsPage }) => { + await expect( + dailyDataDetailsPage.page.locator("footer"), + ).toBeAttached(); }); }); test.describe("receiver user - happy path", () => { test.use({ storageState: "e2e/.auth/receiver.json" }); - test.beforeEach(async ({ page }) => { - await reportDetails.mockGetReportDeliveryResponse(page, id); - await reportDetails.mockGetReportFacilitiesResponse(page, id); - await reportDetails.goto(page, id); - - await page.getByRole("table").waitFor({ state: "visible" }); + test.beforeEach(async ({ dailyDataDetailsPage }) => { + await dailyDataDetailsPage.page + .getByRole("table") + .waitFor({ state: "visible" }); }); - test("has correct title", async ({ page }) => { - await title(page); + test("has correct title", async ({ dailyDataDetailsPage }) => { + await title(dailyDataDetailsPage.page); }); test.describe("table", () => { - test("has correct headers", async ({ page }) => { - await detailsTableHeaders(page); + test("has correct headers", async ({ dailyDataDetailsPage }) => { + await detailsTableHeaders(dailyDataDetailsPage.page); }); - test("'Facility' column has expected data", async ({ page }) => { - expect(await tableDataCellValue(page, 0, 0)).toEqual( - "Any lab USA 1", - ); + test("'Facility' column has expected data", async ({ + dailyDataDetailsPage, + }) => { + expect( + await tableDataCellValue(dailyDataDetailsPage.page, 0, 0), + ).toEqual("Any lab USA 1"); }); - test("'Location' column has expected data", async ({ page }) => { - expect(await tableDataCellValue(page, 0, 1)).toEqual( - "Juneau, AK", - ); + test("'Location' column has expected data", async ({ + dailyDataDetailsPage, + }) => { + expect( + await tableDataCellValue(dailyDataDetailsPage.page, 0, 1), + ).toEqual("Juneau, AK"); }); - test("'CLIA' column has expected data", async ({ page }) => { - expect(await tableDataCellValue(page, 0, 2)).toEqual( - "34D8574402", - ); + test("'CLIA' column has expected data", async ({ + dailyDataDetailsPage, + }) => { + expect( + await tableDataCellValue(dailyDataDetailsPage.page, 0, 2), + ).toEqual("34D8574402"); }); - test("'Total tests' column has expected data", async ({ page }) => { - expect(await tableDataCellValue(page, 0, 3)).toEqual("10"); + test("'Total tests' column has expected data", async ({ + dailyDataDetailsPage, + }) => { + expect( + await tableDataCellValue(dailyDataDetailsPage.page, 0, 3), + ).toEqual("10"); }); test("'Total positive' column has expected data", async ({ - page, + dailyDataDetailsPage, }) => { - expect(await tableDataCellValue(page, 0, 4)).toEqual("1"); + expect( + await tableDataCellValue(dailyDataDetailsPage.page, 0, 4), + ).toEqual("1"); }); }); - test("should download file", async ({ page }) => { - await reportDetails.downloadFile(page, id, fileName); + test("should download file", async ({ dailyDataDetailsPage }) => { + await reportDetails.downloadFile( + dailyDataDetailsPage.page, + id, + fileName, + ); }); - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); + test("has footer", async ({ dailyDataDetailsPage }) => { + await expect( + dailyDataDetailsPage.page.locator("footer"), + ).toBeAttached(); }); }); test.describe("receiver user - server error", () => { test.use({ storageState: "e2e/.auth/receiver.json" }); - test.beforeEach(async ({ page }) => { - await reportDetails.mockGetReportDeliveryResponse(page, id, 500); - await reportDetails.goto(page, id); + test.beforeEach(async ({ dailyDataDetailsPage }) => { + await reportDetails.mockGetReportDeliveryResponse( + dailyDataDetailsPage.page, + id, + 500, + ); + await reportDetails.goto(dailyDataDetailsPage.page, id); }); - test("has alert", async ({ page }) => { - await expect(page.getByTestId("alert")).toBeAttached(); + test("has alert", async ({ dailyDataDetailsPage }) => { await expect( - page.getByText( + dailyDataDetailsPage.page.getByTestId("alert"), + ).toBeAttached(); + await expect( + dailyDataDetailsPage.page.getByText( /Our apologies, there was an error loading this content./, ), ).toBeAttached(); }); - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); + test("has footer", async ({ dailyDataDetailsPage }) => { + await expect( + dailyDataDetailsPage.page.locator("footer"), + ).toBeAttached(); }); }); test.describe("sender user", () => { test.use({ storageState: "e2e/.auth/sender.json" }); - test.beforeEach(async ({ page }) => { - await reportDetails.goto(page, id); - }); + // test.beforeEach(async ({ dailyDataDetailsPage }) => { + // await reportDetails.goto(dailyDataDetailsPage.page, id); + // }); - test("has alert", async ({ page }) => { - await expect(page.getByTestId("alert")).toBeAttached(); + test("has alert", async ({ dailyDataDetailsPage }) => { + dailyDataDetailsPage.mockError = true; + await dailyDataDetailsPage.reload(); + + await expect( + dailyDataDetailsPage.page.getByTestId("alert"), + ).toBeAttached(); await expect( - page.getByText( + dailyDataDetailsPage.page.getByText( /Our apologies, there was an error loading this content./, ), ).toBeAttached(); }); - test("has footer", async ({ page }) => { - await expect(page.locator("footer")).toBeAttached(); + test("has footer", async ({ dailyDataDetailsPage }) => { + await expect( + dailyDataDetailsPage.page.locator("footer"), + ).toBeAttached(); }); }); }); diff --git a/frontend-react/e2e/spec/all/daily-data-page.spec.ts b/frontend-react/e2e/spec/all/daily-data-page.spec.ts index 2d6a2447d0c..98a4a680e91 100644 --- a/frontend-react/e2e/spec/all/daily-data-page.spec.ts +++ b/frontend-react/e2e/spec/all/daily-data-page.spec.ts @@ -18,6 +18,8 @@ import { endTimeClear, filterReset, filterStatus, + mockGetOrgAlaskaReceiversResponse, + mockGetOrgIgnoreReceiversResponse, receiverDropdown, searchButton, searchInput, @@ -29,10 +31,6 @@ import { startTimeClear, tableHeaders, } from "../../pages/daily-data"; -import { - mockGetOrgAlaskaReceiversResponse, - mockGetOrgIgnoreReceiversResponse, -} from "../../pages/organization"; import { mockGetDeliveriesForOrgAlaskaResponse, mockGetDeliveriesForOrgIgnoreResponse, diff --git a/frontend-react/e2e/spec/all/homepage.spec.ts b/frontend-react/e2e/spec/all/homepage.spec.ts index 6c2d1c070a1..6d1b4bb9d08 100644 --- a/frontend-react/e2e/spec/all/homepage.spec.ts +++ b/frontend-react/e2e/spec/all/homepage.spec.ts @@ -7,67 +7,77 @@ import * as managingYourConnection from "../../pages/managing-your-connection"; import * as ourNetwork from "../../pages/our-network"; import * as security from "../../pages/security"; -test.describe("Homepage", () => { - test.beforeEach(async ({ page }) => { - await homepage.goto(page); - }); +test.describe( + "Homepage", + { + tag: "@smoke", + }, + () => { + test.beforeEach(async ({ page }) => { + await homepage.goto(page); + }); - test("has correct title", async ({ page }) => { - await expect(page).toHaveTitle( - /ReportStream - CDC's free, interoperable data transfer platform/, - ); - }); + test("has correct title", async ({ page }) => { + await expect(page).toHaveTitle( + /ReportStream - CDC's free, interoperable data transfer platform/, + ); + }); - test("opens the Security page on 'security of your data' click", async ({ - page, - }) => { - await page.getByRole("link", { name: "security of your data" }).click(); - await security.onLoad(page); - // Go back to the homepage - await header.clickOnHome(page); + test("opens the Security page on 'security of your data' click", async ({ + page, + }) => { + await page + .getByRole("link", { name: "security of your data" }) + .click(); + await security.onLoad(page); + // Go back to the homepage + await header.clickOnHome(page); - expect(true).toBe(true); - }); + expect(true).toBe(true); + }); - test("opens the managing-your-connection page on 'our tools' click", async ({ - page, - }) => { - await page.getByRole("link", { name: "our tools" }).click(); - await managingYourConnection.onLoad(page); - // Go back to the homepage - await header.clickOnHome(page); + test("opens the managing-your-connection page on 'our tools' click", async ({ + page, + }) => { + await page.getByRole("link", { name: "our tools" }).click(); + await managingYourConnection.onLoad(page); + // Go back to the homepage + await header.clickOnHome(page); - expect(true).toBe(true); - }); + expect(true).toBe(true); + }); - test("opens Our Network page on 'See our full network' click", async ({ - page, - }) => { - await page.getByRole("link", { name: "See our full network" }).click(); - await ourNetwork.onLoad(page); - // Go back to the homepage - await header.clickOnHome(page); + test("opens Our Network page on 'See our full network' click", async ({ + page, + }) => { + await page + .getByRole("link", { name: "See our full network" }) + .click(); + await ourNetwork.onLoad(page); + // Go back to the homepage + await header.clickOnHome(page); - expect(true).toBe(true); - }); + expect(true).toBe(true); + }); - test("is clickable Where were live map", async ({ page }) => { - // Trigger map click and go to our network page - await ourNetwork.clickOnLiveMap(page); - // Go back to the homepage - await header.clickOnHome(page); + test("is clickable Where were live map", async ({ page }) => { + // Trigger map click and go to our network page + await ourNetwork.clickOnLiveMap(page); + // Go back to the homepage + await header.clickOnHome(page); - expect(true).toBe(true); - }); + expect(true).toBe(true); + }); - test("explicit scroll to footer and then scroll to top", async ({ - page, - }) => { - await expect(page.locator("footer")).not.toBeInViewport(); - await scrollToFooter(page); - await expect(page.locator("footer")).toBeInViewport(); - await expect(page.getByTestId("govBanner")).not.toBeInViewport(); - await scrollToTop(page); - await expect(page.getByTestId("govBanner")).toBeInViewport(); - }); -}); + test("explicit scroll to footer and then scroll to top", async ({ + page, + }) => { + await expect(page.locator("footer")).not.toBeInViewport(); + await scrollToFooter(page); + await expect(page.locator("footer")).toBeInViewport(); + await expect(page.getByTestId("govBanner")).not.toBeInViewport(); + await scrollToTop(page); + await expect(page.getByTestId("govBanner")).toBeInViewport(); + }); + }, +); diff --git a/frontend-react/e2e/spec/all/idletimeout.spec.ts b/frontend-react/e2e/spec/all/idletimeout.spec.ts index 2f2c02604bf..3d2ae35eb96 100644 --- a/frontend-react/e2e/spec/all/idletimeout.spec.ts +++ b/frontend-react/e2e/spec/all/idletimeout.spec.ts @@ -1,26 +1,63 @@ -import { expect, test } from "@playwright/test"; +import { expect } from "@playwright/test"; import process from "node:process"; -import * as organization from "../../pages/organization"; +import { OrganizationPage } from "../../pages/organization"; +import { test as baseTest } from "../../test"; const timeout = parseInt(process.env.VITE_IDLE_TIMEOUT ?? "20000"); // Add/Sub 500 ms to account for variance const timeoutLow = timeout - 500; const timeoutHigh = timeout + 500; -test.use({ storageState: "e2e/.auth/admin.json" }); +export interface OrganizationPageFixtures { + organizationPage: OrganizationPage; +} + +const test = baseTest.extend({ + organizationPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new OrganizationPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog + }); + await page.goto(); + await use(page); + }, +}); -test.skip("Does not trigger early", async ({ page }) => { - await organization.goto(page); +test.use({ storageState: "e2e/.auth/admin.json" }); - await expect(page.getByRole("banner").first()).toBeVisible(); - await page.keyboard.down("Tab"); +test.skip("Does not trigger early", async ({ organizationPage }) => { + await expect( + organizationPage.page.getByRole("banner").first(), + ).toBeVisible(); + await organizationPage.page.keyboard.down("Tab"); const start = new Date(); - await page.waitForRequest(/\/oauth2\/default\/v1\/revoke/, { - timeout: timeoutHigh, - }); + await organizationPage.page.waitForRequest( + /\/oauth2\/default\/v1\/revoke/, + { + timeout: timeoutHigh, + }, + ); const end = new Date(); diff --git a/frontend-react/e2e/spec/all/managing-your-connection-page.spec.ts b/frontend-react/e2e/spec/all/managing-your-connection-page.spec.ts new file mode 100644 index 00000000000..9e3d7ceabc0 --- /dev/null +++ b/frontend-react/e2e/spec/all/managing-your-connection-page.spec.ts @@ -0,0 +1,124 @@ +import { scrollToFooter, scrollToTop } from "../../helpers/utils"; +import { ManagingYourConnectionPage } from "../../pages/managing-your-connection"; +import { test as baseTest, expect } from "../../test"; + +const cards = [ + { + name: "For healthcare organizations", + links: [ + "Manage your public key", + "View your submission history", + "Login", + "contact us", + ], + }, + { + name: "For public health agencies", + links: [ + "Refer healthcare organizations", + "View your dashboard", + "Login", + "contact us", + ], + }, +]; + +export interface ManagingYourConnectionPageFixtures { + managingYourConnectionPage: ManagingYourConnectionPage; +} + +const test = baseTest.extend({ + managingYourConnectionPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }, + use, + ) => { + const page = new ManagingYourConnectionPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe( + "Managing Your Connection page", + { + tag: "@smoke", + }, + () => { + test("has correct title", async ({ managingYourConnectionPage }) => { + await expect(managingYourConnectionPage.page).toHaveTitle( + managingYourConnectionPage.title, + ); + await expect(managingYourConnectionPage.heading).toBeVisible(); + }); + + test.describe("Quick links", () => { + for (const card of cards) { + test(`should have ${card.name} links`, async ({ + managingYourConnectionPage, + }) => { + const cardHeader = managingYourConnectionPage.page.locator( + ".usa-card__header", + { + hasText: card.name, + }, + ); + + await expect(cardHeader).toBeVisible(); + + const cardContainer = cardHeader.locator(".."); + + for (const link of card.links) { + await expect( + cardContainer.getByRole("link", { + name: `${link}`, + }), + ).toBeVisible(); + } + }); + } + }); + + test.describe("Footer", () => { + test("has footer", async ({ managingYourConnectionPage }) => { + await expect(managingYourConnectionPage.footer).toBeAttached(); + }); + + test("explicit scroll to footer and then scroll to top", async ({ + managingYourConnectionPage, + }) => { + await expect( + managingYourConnectionPage.footer, + ).not.toBeInViewport(); + await scrollToFooter(managingYourConnectionPage.page); + await expect( + managingYourConnectionPage.footer, + ).toBeInViewport(); + await expect( + managingYourConnectionPage.page.getByTestId("govBanner"), + ).not.toBeInViewport(); + await scrollToTop(managingYourConnectionPage.page); + await expect( + managingYourConnectionPage.page.getByTestId("govBanner"), + ).toBeInViewport(); + }); + }); + }, +); diff --git a/frontend-react/e2e/spec/all/organization-settings-page.spec.ts b/frontend-react/e2e/spec/all/organization-settings-page.spec.ts index 2461b53e473..70c3231f50d 100644 --- a/frontend-react/e2e/spec/all/organization-settings-page.spec.ts +++ b/frontend-react/e2e/spec/all/organization-settings-page.spec.ts @@ -1,33 +1,64 @@ -import { expect, test } from "@playwright/test"; +import { expect } from "@playwright/test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { MOCK_GET_ORGANIZATION_SETTINGS_LIST } from "../../mocks/organizations"; -import * as organization from "../../pages/organization"; +import { OrganizationPage } from "../../pages/organization"; +import { test as baseTest } from "../../test"; const __dirname = fileURLToPath(import.meta.url); +export interface OrganizationPageFixtures { + organizationPage: OrganizationPage; +} + +const test = baseTest.extend({ + organizationPage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }, + use, + ) => { + const page = new OrganizationPage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + frontendWarningsLogPath, + isFrontendWarningsLog, + }); + await page.goto(); + await use(page); + }, +}); + test.describe("Admin Organization Settings Page", () => { test.describe("not authenticated", () => { - test("redirects to login", async ({ page }) => { - await organization.goto(page); - await expect(page).toHaveURL("/login"); + test("redirects to login", async ({ organizationPage }) => { + await expect(organizationPage.page).toHaveURL("/login"); }); }); test.describe("authenticated receiver", () => { test.use({ storageState: "e2e/.auth/receiver.json" }); - test("returns Page Not Found", async ({ page }) => { - await organization.goto(page); - await expect(page).toHaveTitle(/Page Not Found/); + test("returns Page Not Found", async ({ organizationPage }) => { + await expect(organizationPage.page).toHaveTitle(/Page Not Found/); }); }); test.describe("authenticated sender", () => { test.use({ storageState: "e2e/.auth/sender.json" }); - test("returns Page Not Found", async ({ page }) => { - await organization.goto(page); - await expect(page).toHaveTitle(/Page Not Found/); + test("returns Page Not Found", async ({ organizationPage }) => { + await expect(organizationPage.page).toHaveTitle(/Page Not Found/); }); }); @@ -35,215 +66,240 @@ test.describe("Admin Organization Settings Page", () => { test.use({ storageState: "e2e/.auth/admin.json" }); test("If there is an error, the error is shown on the page", async ({ - page, + organizationPage, }) => { - await page.route(organization.API_ORGANIZATIONS, (route) => - route.fulfill({ status: 500 }), - ); - await organization.goto(page); - await expect(page.getByText("there was an error")).toBeVisible(); + organizationPage.mockError = true; + await organizationPage.reload(); + await expect( + organizationPage.page.getByText("there was an error"), + ).toBeVisible(); }); - test.describe("When there is no error", () => { - test.beforeEach(async ({ page }) => { - await page.route(organization.API_ORGANIZATIONS, (route) => - route.fulfill({ - status: 200, - json: MOCK_GET_ORGANIZATION_SETTINGS_LIST, - }), - ); - await organization.goto(page); - }); - - test("nav contains the 'Admin tools' dropdown with 'Organization Settings' option", async ({ - page, - }) => { - const navItems = page.locator(".usa-nav li"); - await expect(navItems).toContainText(["Admin tools"]); - - await page - .getByTestId("auth-header") - .getByTestId("navDropDownButton") - .getByText("Admin tools") - .click(); - - expect(page.getByText("Organization Settings")).toBeTruthy(); - - await page.getByText("Organization Settings").click(); - await expect(page).toHaveURL("/admin/settings"); - }); - - test("Has correct title", async ({ page }) => { - await expect(page).toHaveURL(/settings/); - await expect(page).toHaveTitle(/Admin-Organizations/); - }); - - test("Displays data", async ({ page }) => { - // Heading with result length - await expect( - page.getByRole("heading", { - name: `Organizations (${MOCK_GET_ORGANIZATION_SETTINGS_LIST.length})`, - }), - ).toBeVisible(); - - // Table - // empty is button column - const colHeaders = [ - "Name", - "Description", - "Jurisdiction", - "State", - "County", - "", - ]; - // include header row - const rowCount = MOCK_GET_ORGANIZATION_SETTINGS_LIST.length + 1; - const table = page.getByRole("table"); - await expect(table).toBeVisible(); - const rows = await table.getByRole("row").all(); - expect(rows).toHaveLength(rowCount); - for (const [i, row] of rows.entries()) { - const cols = await row.getByRole("cell").allTextContents(); - expect(cols).toHaveLength(colHeaders.length); - - const { description, jurisdiction, name, stateCode } = - i === 0 - ? MOCK_GET_ORGANIZATION_SETTINGS_LIST[0] - : MOCK_GET_ORGANIZATION_SETTINGS_LIST.find( - (i) => i.name === cols[0], - ) ?? { name: "INVALID" }; - // if first row, we expect column headers. else, the data row matching id (name) - // SetEdit is text of buttons in button column - const expectedColContents = - i === 0 - ? colHeaders - : [ - name, - description ?? "", - jurisdiction ?? "", - stateCode ?? "", - "", - "SetEdit", - ]; - - for (const [i, col] of cols.entries()) { - expect(col).toBe(expectedColContents[i]); - } - } - }); - - test("Create new organization navigation works", async ({ - page, - }) => { - const link = page.getByRole("link", { - name: "Create New Organization", + test.describe( + "When there is no error", + { + tag: "@smoke", + }, + () => { + test("nav contains the 'Admin tools' dropdown with 'Organization Settings' option", async ({ + organizationPage, + }) => { + const navItems = + organizationPage.page.locator(".usa-nav li"); + await expect(navItems).toContainText(["Admin tools"]); + + await organizationPage.page + .getByTestId("auth-header") + .getByTestId("navDropDownButton") + .getByText("Admin tools") + .click(); + + expect( + organizationPage.page.getByText( + "Organization Settings", + ), + ).toBeTruthy(); + + await organizationPage.page + .getByText("Organization Settings") + .click(); + await expect(organizationPage.page).toHaveURL( + "/admin/settings", + ); }); - const expectedUrl = "/admin/new/org"; - await expect(link).toBeVisible(); - await link.click(); - await page.waitForURL(expectedUrl); - await expect(page.getByRole("heading")).toBeVisible(); - - expect(page.url()).toContain(expectedUrl); - }); - - test("Save CSV button downloads a file", async ({ page }) => { - const downloadProm = page.waitForEvent("download"); - const saveButton = page.getByRole("button", { - name: "Save List to CSV", + test("Has correct title", async ({ organizationPage }) => { + await expect(organizationPage.page).toHaveURL(/settings/); + await expect(organizationPage.page).toHaveTitle( + /Admin-Organizations/, + ); }); - await expect(saveButton).toBeVisible(); - await saveButton.click(); - const download = await downloadProm; - - const expectedFile = readFileSync( - join(__dirname, "../../../mocks/prime-orgs.csv"), - { encoding: "utf-8" }, - ); - const stream = await download.createReadStream(); - const file = (await stream.toArray()).toString(); - expect(file).toBe(expectedFile); - expect(download.suggestedFilename()).toBe("prime-orgs.csv"); - }); - - test("Filtering works", async ({ page }) => { - const table = page.getByRole("table"); - const { description, name, jurisdiction, stateCode } = - MOCK_GET_ORGANIZATION_SETTINGS_LIST[2]; - const filterBox = page.getByRole("textbox", { - name: "Filter:", + test("Displays data", async ({ organizationPage }) => { + // Heading with result length + await expect( + organizationPage.page.getByRole("heading", { + name: `Organizations (${MOCK_GET_ORGANIZATION_SETTINGS_LIST.length})`, + }), + ).toBeVisible(); + + // Table + // empty is button column + const colHeaders = [ + "Name", + "Description", + "Jurisdiction", + "State", + "County", + "", + ]; + // include header row + const rowCount = + MOCK_GET_ORGANIZATION_SETTINGS_LIST.length + 1; + const table = organizationPage.page.getByRole("table"); + await expect(table).toBeVisible(); + const rows = await table.getByRole("row").all(); + expect(rows).toHaveLength(rowCount); + for (const [i, row] of rows.entries()) { + const cols = await row + .getByRole("cell") + .allTextContents(); + expect(cols).toHaveLength(colHeaders.length); + + const { description, jurisdiction, name, stateCode } = + i === 0 + ? MOCK_GET_ORGANIZATION_SETTINGS_LIST[0] + : MOCK_GET_ORGANIZATION_SETTINGS_LIST.find( + (i) => i.name === cols[0], + ) ?? { name: "INVALID" }; + // if first row, we expect column headers. else, the data row matching id (name) + // SetEdit is text of buttons in button column + const expectedColContents = + i === 0 + ? colHeaders + : [ + name, + description ?? "", + jurisdiction ?? "", + stateCode ?? "", + "", + "SetEdit", + ]; + + for (const [i, col] of cols.entries()) { + expect(col).toBe(expectedColContents[i]); + } + } }); - await expect(filterBox).toBeVisible(); - - await filterBox.fill(name); - const rows = await table.getByRole("row").all(); - expect(rows).toHaveLength(2); - const cols = rows[1].getByRole("cell").allTextContents(); - const expectedColContents = [ - name, - description ?? "", - jurisdiction ?? "", - stateCode ?? "", - "", - "SetEdit", - ]; - - for (const [i, col] of (await cols).entries()) { - expect(col).toBe(expectedColContents[i]); - } - }); - - test('Clicking "Set" updates link label', async ({ page }) => { - const firstDataRow = page - .getByRole("table") - .getByRole("row") - .nth(1); - const firstDataRowName = - (await firstDataRow - .getByRole("cell") - .nth(0) - .textContent()) ?? "INVALID"; - const setButton = firstDataRow.getByRole("button", { - name: "Set", + test("Create new organization navigation works", async ({ + organizationPage, + }) => { + const link = organizationPage.page.getByRole("link", { + name: "Create New Organization", + }); + const expectedUrl = "/admin/new/org"; + + await expect(link).toBeVisible(); + await link.click(); + await organizationPage.page.waitForURL(expectedUrl); + await expect( + organizationPage.page.getByRole("heading"), + ).toBeVisible(); + + expect(organizationPage.page.url()).toContain(expectedUrl); }); - await expect(setButton).toBeVisible(); - await setButton.click(); - - const orgLink = page.getByRole("link", { - name: firstDataRowName, + test("Save CSV button downloads a file", async ({ + organizationPage, + }) => { + const downloadProm = + organizationPage.page.waitForEvent("download"); + const saveButton = organizationPage.page.getByRole( + "button", + { + name: "Save List to CSV", + }, + ); + + await expect(saveButton).toBeVisible(); + await saveButton.click(); + const download = await downloadProm; + + const expectedFile = readFileSync( + join(__dirname, "../../../mocks/prime-orgs.csv"), + { encoding: "utf-8" }, + ); + const stream = await download.createReadStream(); + const file = (await stream.toArray()).toString(); + expect(file).toBe(expectedFile); + expect(download.suggestedFilename()).toBe("prime-orgs.csv"); }); - await expect(orgLink).toBeVisible(); - await expect(orgLink).toHaveAttribute( - "href", - "/admin/settings", - ); - }); - - test("Edit navigation works", async ({ page }) => { - const firstDataRow = page - .getByRole("table") - .getByRole("row") - .nth(1); - const firstDataRowName = await firstDataRow - .getByRole("cell") - .nth(0) - .textContent(); - const expectedUrl = `/admin/orgsettings/org/${firstDataRowName}`; - const editButton = firstDataRow.getByRole("button", { - name: "Edit", + + test("Filtering works", async ({ organizationPage }) => { + const table = organizationPage.page.getByRole("table"); + const { description, name, jurisdiction, stateCode } = + MOCK_GET_ORGANIZATION_SETTINGS_LIST[2]; + const filterBox = organizationPage.page.getByRole( + "textbox", + { + name: "Filter:", + }, + ); + + await expect(filterBox).toBeVisible(); + + await filterBox.fill(name); + const rows = await table.getByRole("row").all(); + expect(rows).toHaveLength(2); + const cols = rows[1].getByRole("cell").allTextContents(); + const expectedColContents = [ + name, + description ?? "", + jurisdiction ?? "", + stateCode ?? "", + "", + "SetEdit", + ]; + + for (const [i, col] of (await cols).entries()) { + expect(col).toBe(expectedColContents[i]); + } }); - await expect(editButton).toBeVisible(); - await editButton.click(); - await page.waitForURL(expectedUrl); - await expect(page.getByRole("heading")).toBeVisible(); + test('Clicking "Set" updates link label', async ({ + organizationPage, + }) => { + const firstDataRow = organizationPage.page + .getByRole("table") + .getByRole("row") + .nth(1); + const firstDataRowName = + (await firstDataRow + .getByRole("cell") + .nth(0) + .textContent()) ?? "INVALID"; + const setButton = firstDataRow.getByRole("button", { + name: "Set", + }); + + await expect(setButton).toBeVisible(); + await setButton.click(); + + const orgLink = organizationPage.page.getByRole("link", { + name: firstDataRowName, + }); + await expect(orgLink).toBeVisible(); + await expect(orgLink).toHaveAttribute( + "href", + "/admin/settings", + ); + }); - expect(page.url()).toContain(expectedUrl); - }); - }); + test("Edit navigation works", async ({ organizationPage }) => { + const firstDataRow = organizationPage.page + .getByRole("table") + .getByRole("row") + .nth(1); + const firstDataRowName = await firstDataRow + .getByRole("cell") + .nth(0) + .textContent(); + const expectedUrl = `/admin/orgsettings/org/${firstDataRowName}`; + const editButton = firstDataRow.getByRole("button", { + name: "Edit", + }); + + await expect(editButton).toBeVisible(); + await editButton.click(); + await organizationPage.page.waitForURL(expectedUrl); + await expect( + organizationPage.page.getByRole("heading"), + ).toBeVisible(); + + expect(organizationPage.page.url()).toContain(expectedUrl); + }); + }, + ); }); }); diff --git a/frontend-react/e2e/spec/all/our-network-page.spec.ts b/frontend-react/e2e/spec/all/our-network-page.spec.ts index 7183dad9c84..d8345618291 100644 --- a/frontend-react/e2e/spec/all/our-network-page.spec.ts +++ b/frontend-react/e2e/spec/all/our-network-page.spec.ts @@ -2,44 +2,50 @@ import { expect, test } from "@playwright/test"; import * as sideNav from "../../pages/about-side-navigation"; import * as ourNetwork from "../../pages/our-network"; -test.describe("Our network page", () => { - test.beforeEach(async ({ page }) => { - await ourNetwork.goto(page); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveTitle(/Our network/); - }); - - test.describe("Side navigation", () => { - test("has Our network link", async ({ page }) => { - await sideNav.clickNetwork(page); - await expect(page).toHaveURL(/.*about\/our-network/); +test.describe( + "Our network page", + { + tag: "@smoke", + }, + () => { + test.beforeEach(async ({ page }) => { + await ourNetwork.goto(page); }); - test("has Product roadmap link", async ({ page }) => { - await sideNav.clickRoadmap(page); - await expect(page).toHaveURL(/.*about\/roadmap/); + test("has correct title", async ({ page }) => { + await expect(page).toHaveTitle(/Our network/); }); - test("has News link", async ({ page }) => { - await sideNav.clickNews(page); - await expect(page).toHaveURL(/.*about\/news/); + test.describe("Side navigation", () => { + test("has Our network link", async ({ page }) => { + await sideNav.clickNetwork(page); + await expect(page).toHaveURL(/.*about\/our-network/); + }); + + test("has Product roadmap link", async ({ page }) => { + await sideNav.clickRoadmap(page); + await expect(page).toHaveURL(/.*about\/roadmap/); + }); + + test("has News link", async ({ page }) => { + await sideNav.clickNews(page); + await expect(page).toHaveURL(/.*about\/news/); + }); + + test("has Case studies link", async ({ page }) => { + await sideNav.clickCaseStudies(page); + await expect(page).toHaveURL(/.*about\/case-studies/); + }); + + test("has Security link", async ({ page }) => { + await sideNav.clickSecurity(page); + await expect(page).toHaveURL(/.*about\/security/); + }); + + test("has Release notes link", async ({ page }) => { + await sideNav.clickReleaseNotes(page); + await expect(page).toHaveURL(/.*about\/release-notes/); + }); }); - - test("has Case studies link", async ({ page }) => { - await sideNav.clickCaseStudies(page); - await expect(page).toHaveURL(/.*about\/case-studies/); - }); - - test("has Security link", async ({ page }) => { - await sideNav.clickSecurity(page); - await expect(page).toHaveURL(/.*about\/security/); - }); - - test("has Release notes link", async ({ page }) => { - await sideNav.clickReleaseNotes(page); - await expect(page).toHaveURL(/.*about\/release-notes/); - }); - }); -}); + }, +); diff --git a/frontend-react/e2e/spec/all/refer-healthcare-page.spec.ts b/frontend-react/e2e/spec/all/refer-healthcare-page.spec.ts new file mode 100644 index 00000000000..37421d676e4 --- /dev/null +++ b/frontend-react/e2e/spec/all/refer-healthcare-page.spec.ts @@ -0,0 +1,85 @@ +import { scrollToFooter, scrollToTop } from "../../helpers/utils"; +import { ReferHealthcarePage } from "../../pages/refer-healthcare"; +import { test as baseTest, expect } from "../../test"; + +export interface ReferHealthcarePageFixtures { + referHealthcarePage: ReferHealthcarePage; +} + +const test = baseTest.extend({ + referHealthcarePage: async ( + { + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }, + use, + ) => { + const page = new ReferHealthcarePage({ + page: _page, + isMockDisabled, + adminLogin, + senderLogin, + receiverLogin, + storageState, + isFrontendWarningsLog, + frontendWarningsLogPath, + }); + await page.goto(); + await use(page); + }, +}); + +test.describe( + "Managing Your Connection page", + { + tag: "@smoke", + }, + () => { + test("has correct title", async ({ referHealthcarePage }) => { + await expect(referHealthcarePage.page).toHaveTitle(referHealthcarePage.title); + await expect(referHealthcarePage.heading).toBeVisible(); + }); + + test("has correct sidenav items", async ({ referHealthcarePage }) => { + await expect( + referHealthcarePage.page + .getByTestId("sidenav") + .getByRole("link", { name: /Refer healthcare organizations/ }), + ).toBeVisible(); + await expect( + referHealthcarePage.page.getByTestId("sidenav").getByRole("link", { name: /Manage public key/ }), + ).toBeVisible(); + }); + + test("has link to referral email template", async ({ referHealthcarePage }) => { + await expect( + referHealthcarePage.page.getByRole("link", { name: /referral email template/i }), + ).toBeVisible(); + + await referHealthcarePage.page.getByRole("link", { name: /referral email template/i }).click(); + + await expect(referHealthcarePage.page.locator("#email-template")).toBeVisible(); + }); + + test.describe("Footer", () => { + test("has footer", async ({ referHealthcarePage }) => { + await expect(referHealthcarePage.footer).toBeAttached(); + }); + + test("explicit scroll to footer and then scroll to top", async ({ referHealthcarePage }) => { + await expect(referHealthcarePage.footer).not.toBeInViewport(); + await scrollToFooter(referHealthcarePage.page); + await expect(referHealthcarePage.footer).toBeInViewport(); + await expect(referHealthcarePage.page.getByTestId("govBanner")).not.toBeInViewport(); + await scrollToTop(referHealthcarePage.page); + await expect(referHealthcarePage.page.getByTestId("govBanner")).toBeInViewport(); + }); + }); + }, +); diff --git a/frontend-react/e2e/spec/all/roadmap.spec.ts b/frontend-react/e2e/spec/all/roadmap.spec.ts index 7ab1f0cc5b7..7ceda10b462 100644 --- a/frontend-react/e2e/spec/all/roadmap.spec.ts +++ b/frontend-react/e2e/spec/all/roadmap.spec.ts @@ -11,126 +11,135 @@ import { ELC } from "../../helpers/internal-links"; import * as sideNav from "../../pages/about-side-navigation"; import * as roadmap from "../../pages/roadmap"; import { URL_ROADMAP } from "../../pages/roadmap"; -test.describe("Product roadmap page", () => { - test.beforeEach(async ({ page }) => { - await roadmap.goto(page); - }); - - test("has correct title", async ({ page }) => { - await expect(page).toHaveURL(URL_ROADMAP); - await expect(page).toHaveTitle(/Product roadmap/); - }); - - test.describe("Side navigation", () => { - test("has Our network link", async ({ page }) => { - await sideNav.clickNetwork(page); - await expect(page).toHaveURL(/.*about\/our-network/); - }); - - test("has Product roadmap link", async ({ page }) => { - await sideNav.clickRoadmap(page); - await expect(page).toHaveURL(/.*about\/roadmap/); - }); - - test("has News link", async ({ page }) => { - await sideNav.clickNews(page); - await expect(page).toHaveURL(/.*about\/news/); - }); - - test("has Case studies link", async ({ page }) => { - await sideNav.clickCaseStudies(page); - await expect(page).toHaveURL(/.*about\/case-studies/); - }); - test("has Security link", async ({ page }) => { - await sideNav.clickSecurity(page); - await expect(page).toHaveURL(/.*about\/security/); +test.describe( + "Product roadmap page", + { + tag: "@smoke", + }, + () => { + test.beforeEach(async ({ page }) => { + await roadmap.goto(page); }); - test("has Release notes link", async ({ page }) => { - await sideNav.clickReleaseNotes(page); - await expect(page).toHaveURL(/.*about\/release-notes/); - }); - }); - - test.describe("Article Links", () => { - test("has 'ELC-funded'", async ({ page }) => { - const linksCount = page - .locator("article") - .getByRole("link", { name: "ELC-funded" }); - await expect(linksCount).toHaveCount(2); - const newTabPromise = page.waitForEvent("popup"); - await linksCount.nth(1).click(); - const newTab = await newTabPromise; - await newTab.waitForLoadState(); - await expect(newTab).toHaveURL(ELC); + test("has correct title", async ({ page }) => { + await expect(page).toHaveURL(URL_ROADMAP); + await expect(page).toHaveTitle(/Product roadmap/); }); - test("has 'SimpleReport'", async ({ page }) => { - const newTab = await externalLinks.clickOnExternalLink( - "article", - "SimpleReport", - page, - ); - await expect(newTab).toHaveURL(SIMPLEREPORT); - }); - - test("has 'RADx MARS'", async ({ page }) => { - const newTab = await externalLinks.clickOnExternalLink( - "article", - "RADx MARS", - page, - ); - await expect(newTab).toHaveURL(RADX_MARS); - }); - - // TODO: figure out how to open .pdf docs in playwright - test.skip("has 'NIST HL7 2.5.1'", async ({ page }) => { - await page.getByRole("link", { name: "NIST HL7 2.5.1" }).click(); - await expect(page).toHaveURL( - "https://www.cdc.gov/vaccines/programs/iis/technical-guidance/downloads/hl7guide-1-5-2014-11.pdf", - ); - }); - - test("has 'MakeMyTestCount.org'", async ({ page }) => { - const newTab = await externalLinks.clickOnExternalLink( - "article", - "MakeMyTestCount.org", - page, - ); - await expect(newTab).toHaveURL(MAKE_MY_TEST_COUNT); - }); - }); - - test.describe("Additional resources Links", () => { - test("has News", async ({ page }) => { - await internalLinks.clickOnInternalLink( - "div", - "CardGroup", - "News", - page, - ); - await expect(page).toHaveURL(/.*about\/news/); + test.describe("Side navigation", () => { + test("has Our network link", async ({ page }) => { + await sideNav.clickNetwork(page); + await expect(page).toHaveURL(/.*about\/our-network/); + }); + + test("has Product roadmap link", async ({ page }) => { + await sideNav.clickRoadmap(page); + await expect(page).toHaveURL(/.*about\/roadmap/); + }); + + test("has News link", async ({ page }) => { + await sideNav.clickNews(page); + await expect(page).toHaveURL(/.*about\/news/); + }); + + test("has Case studies link", async ({ page }) => { + await sideNav.clickCaseStudies(page); + await expect(page).toHaveURL(/.*about\/case-studies/); + }); + + test("has Security link", async ({ page }) => { + await sideNav.clickSecurity(page); + await expect(page).toHaveURL(/.*about\/security/); + }); + + test("has Release notes link", async ({ page }) => { + await sideNav.clickReleaseNotes(page); + await expect(page).toHaveURL(/.*about\/release-notes/); + }); }); - test("has Release notes", async ({ page }) => { - await internalLinks.clickOnInternalLink( - "div", - "CardGroup", - "Release notes", - page, - ); - await expect(page).toHaveURL(/.*about\/release-notes/); + test.describe("Article Links", () => { + test("has 'ELC-funded'", async ({ page }) => { + const linksCount = page + .locator("article") + .getByRole("link", { name: "ELC-funded" }); + await expect(linksCount).toHaveCount(2); + const newTabPromise = page.waitForEvent("popup"); + await linksCount.nth(1).click(); + const newTab = await newTabPromise; + await newTab.waitForLoadState(); + await expect(newTab).toHaveURL(ELC); + }); + + test("has 'SimpleReport'", async ({ page }) => { + const newTab = await externalLinks.clickOnExternalLink( + "article", + "SimpleReport", + page, + ); + await expect(newTab).toHaveURL(SIMPLEREPORT); + }); + + test("has 'RADx MARS'", async ({ page }) => { + const newTab = await externalLinks.clickOnExternalLink( + "article", + "RADx MARS", + page, + ); + await expect(newTab).toHaveURL(RADX_MARS); + }); + + // TODO: figure out how to open .pdf docs in playwright + test.skip("has 'NIST HL7 2.5.1'", async ({ page }) => { + await page + .getByRole("link", { name: "NIST HL7 2.5.1" }) + .click(); + await expect(page).toHaveURL( + "https://www.cdc.gov/vaccines/programs/iis/technical-guidance/downloads/hl7guide-1-5-2014-11.pdf", + ); + }); + + test("has 'MakeMyTestCount.org'", async ({ page }) => { + const newTab = await externalLinks.clickOnExternalLink( + "article", + "MakeMyTestCount.org", + page, + ); + await expect(newTab).toHaveURL(MAKE_MY_TEST_COUNT); + }); }); - test("has Developer resources", async ({ page }) => { - await internalLinks.clickOnInternalLink( - "div", - "CardGroup", - "Developer resources", - page, - ); - await expect(page).toHaveURL(/.*developer-resources/); + test.describe("Additional resources Links", () => { + test("has News", async ({ page }) => { + await internalLinks.clickOnInternalLink( + "div", + "CardGroup", + "News", + page, + ); + await expect(page).toHaveURL(/.*about\/news/); + }); + + test("has Release notes", async ({ page }) => { + await internalLinks.clickOnInternalLink( + "div", + "CardGroup", + "Release notes", + page, + ); + await expect(page).toHaveURL(/.*about\/release-notes/); + }); + + test("has Developer resources", async ({ page }) => { + await internalLinks.clickOnInternalLink( + "div", + "CardGroup", + "Developer resources", + page, + ); + await expect(page).toHaveURL(/.*developer-resources/); + }); }); - }); -}); + }, +); diff --git a/frontend-react/e2e/spec/all/support-page.spec.ts b/frontend-react/e2e/spec/all/support-page.spec.ts index dd407e6b0db..0948a85b187 100644 --- a/frontend-react/e2e/spec/all/support-page.spec.ts +++ b/frontend-react/e2e/spec/all/support-page.spec.ts @@ -1,4 +1,5 @@ import site from "../../../src/content/site.json" assert { type: "json" }; +import { scrollToFooter, scrollToTop } from "../../helpers/utils"; import { SupportPage } from "../../pages/support.js"; import { test as baseTest, expect } from "../../test"; @@ -51,6 +52,11 @@ const test = baseTest.extend({ }); test.describe("Support page", () => { + test("has correct title", async ({ supportPage }) => { + await expect(supportPage.page).toHaveTitle(supportPage.title); + await expect(supportPage.heading).toBeVisible(); + }); + test("Should have a way of contacting support", async ({ supportPage }) => { const contactLink = supportPage.page .locator(`a[href="${site.forms.contactUs.url}"]`) @@ -61,7 +67,6 @@ test.describe("Support page", () => { }); for (const card of cards) { - // eslint-disable-next-line playwright/expect-expect test(`should have ${card.name} link`, async ({ supportPage }) => { const cardHeader = supportPage.page.locator(".usa-card__header", { hasText: card.name, @@ -78,4 +83,25 @@ test.describe("Support page", () => { ).toBeVisible(); }); } + + test.describe("Footer", () => { + test("has footer", async ({ supportPage }) => { + await expect(supportPage.footer).toBeAttached(); + }); + + test("explicit scroll to footer and then scroll to top", async ({ + supportPage, + }) => { + await expect(supportPage.footer).not.toBeInViewport(); + await scrollToFooter(supportPage.page); + await expect(supportPage.footer).toBeInViewport(); + await expect( + supportPage.page.getByTestId("govBanner"), + ).not.toBeInViewport(); + await scrollToTop(supportPage.page); + await expect( + supportPage.page.getByTestId("govBanner"), + ).toBeInViewport(); + }); + }); }); diff --git a/frontend-react/e2e/spec/all/timezone.spec.ts b/frontend-react/e2e/spec/all/timezone.spec.ts index 242eb60a198..8c7c8cd3597 100644 --- a/frontend-react/e2e/spec/all/timezone.spec.ts +++ b/frontend-react/e2e/spec/all/timezone.spec.ts @@ -28,7 +28,6 @@ test("playwright/browser timezone parity", async ({ page }) => { const browserEnd = new Date(browserEndIso); expect(now.getTimezoneOffset()).toBe(browserNow.getTimezoneOffset()); - expect(now.toLocaleString()).toBe(browserNow.toLocaleString()); expect(timezoneId).toBe(browserTimezoneId); expect(nowStart.toLocaleString()).toBe(browserStart.toLocaleString()); expect(nowEnd.toLocaleString()).toBe(browserEnd.toLocaleString()); diff --git a/frontend-react/e2e/test.ts b/frontend-react/e2e/test.ts index 88491dbc429..00f822e12ac 100644 --- a/frontend-react/e2e/test.ts +++ b/frontend-react/e2e/test.ts @@ -7,6 +7,9 @@ import { } from "@playwright/test"; import { join } from "node:path"; +// eslint-disable-next-line import/export +export * from "@playwright/test"; + export interface TestLogin { username: string; password: string; @@ -72,6 +75,7 @@ function createLogins( export const logins = createLogins(["admin", "receiver", "sender"]); +// eslint-disable-next-line import/export export const test = base.extend({ adminLogin: { ...logins.admin, @@ -96,4 +100,3 @@ export type TestArgs

= Pick< > & CustomFixtures; -export { expect } from "@playwright/test"; diff --git a/frontend-react/index.html b/frontend-react/index.html index 0c3544f4cb6..4af25538ace 100644 --- a/frontend-react/index.html +++ b/frontend-react/index.html @@ -5,20 +5,11 @@ - + %VITE_TITLE% - - + + @@ -29,10 +20,7 @@ display: none; } - +

diff --git a/frontend-react/lint-staged.config.js b/frontend-react/lint-staged.config.js index b245eae28d3..3a5ed02062e 100644 --- a/frontend-react/lint-staged.config.js +++ b/frontend-react/lint-staged.config.js @@ -6,25 +6,17 @@ export default { */ "{src/AppRouter.tsx,public/sitemap.xml}": (filenames) => "echo 'TODO'", /* Linting and formatting */ - "*.{js,mjs,cjs,ts,mts,cts,tsx}": [ - "eslint --fix", - "prettier --write --list-different", - ], + "*.{js,mjs,cjs,ts,mts,cts,tsx}": ["eslint --fix", "prettier --write --list-different"], "*.{md,css,scss,json}": "prettier --write --check", /** * If either yarn.lock or package.json changed: run dedupe, manually add yarn.lock * (in case it wasn't already staged). */ - "{yarn.lock, package.json}": [ - () => "yarn dedupe", - () => "git add frontend-react/yarn.lock", - ], + "{yarn.lock, package.json}": [() => "yarn dedupe", () => "git add frontend-react/yarn.lock"], /** * Determine if whole project needs testing or not */ - "{**/*.{ts,tsx},yarn.lock,package.json}": ( - /** @type string[] */ filenames, - ) => { + "{**/*.{ts,tsx},yarn.lock,package.json}": (/** @type string[] */ filenames) => { // if dependencies changed, test the whole project if (filenames.some((f) => /(package.json)|(yarn.lock)$/.exec(f))) { return "cross-env TZ=UTC vitest --run --silent"; diff --git a/frontend-react/package.json b/frontend-react/package.json index 4fe49323df7..8c38ed5fd1b 100644 --- a/frontend-react/package.json +++ b/frontend-react/package.json @@ -1,202 +1,204 @@ { - "name": "react-frontend", - "version": "0.1.1", - "private": true, - "type": "module", - "npmClient": "yarn", - "dependencies": { - "@microsoft/applicationinsights-react-js": "^17.3.0", - "@microsoft/applicationinsights-web": "^3.3.0", - "@okta/okta-react": "^6.9.0", - "@okta/okta-signin-widget": "^7.19.6", - "@rest-hooks/rest": "^3.0.3", - "@tanstack/react-query": "^5.49.2", - "@tanstack/react-query-devtools": "^5.49.2", - "@trussworks/react-uswds": "^9.0.0", - "@uswds/uswds": "3.7.1", - "axios": "^1.7.2", - "classnames": "^2.5.1", - "date-fns": "^3.6.0", - "date-fns-tz": "^3.1.3", - "dompurify": "^3.1.6", - "downloadjs": "^1.4.7", - "export-to-csv-fix-source-map": "^0.2.1", - "focus-trap-react": "^10.2.3", - "history": "^5.3.0", - "html-to-text": "^9.0.5", - "lodash": "^4.17.21", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "react-helmet-async": "^2.0.5", - "react-idle-timer": "^5.7.2", - "react-loader-spinner": "^6.1.6", - "react-markdown": "^9.0.1", - "react-query-kit": "^3.3.0", - "react-router": "^6.24.1", - "react-router-dom": "^6.24.1", - "react-scroll-sync": "^0.11.2", - "react-toastify": "^10.0.5", - "rehype-raw": "^7.0.0", - "rehype-slug": "^5.1.0", - "rest-hooks": "^6.1.7", - "sanitize-html": "^2.13.0", - "tsx": "^4.16.2", - "use-deep-compare-effect": "^1.8.1", - "uuid": "^10.0.0", - "web-vitals": "^3.4.0" - }, - "scripts": { - "postinstall": "scripts/postinstall.sh", - "dev": "vite", - "preview": "vite preview --mode preview", - "preview:csp": "yarn run preview --mode csp", - "preview:test": "yarn run preview --mode test", - "preview:ci": "yarn run preview --mode ci", - "preview:build": "yarn run build:production && yarn run preview", - "preview:build:csp": "yarn run build:csp && yarn run preview:csp", - "preview:build:test": "yarn run build:test && yarn run preview:test", - "preview:build:ci": "yarn run build:ci && yarn run preview:ci", - "build:test": "yarn run build-base --mode test", - "build:ci": "yarn run build-base --mode ci", - "build:demo1": "yarn run build-base --mode demo1", - "build:demo2": "yarn run build-base --mode demo2", - "build:demo3": "yarn run build-base --mode demo3", - "build:trialfrontend01": "yarn run build-base --mode trialfrontend01", - "build:trialfrontend02": "yarn run build-base --mode trialfrontend02", - "build:trialfrontend03": "yarn run build-base --mode trialfrontend03", - "build:staging": "yarn run build-base --mode staging", - "build:production": "yarn run build-base", - "build:csp": "yarn run build-base --mode csp", - "build-base": "vite build", - "test": "cross-env vitest", - "test:debug": "cross-env DEBUG_PRINT_LIMIT=100000 vitest --run --no-file-parallelism", - "test:ci": "cross-env VITE_BACKEND_URL=http://localhost vitest --coverage", - "test:ui": "cross-env vitest --ui", - "test:e2e": "playwright test", - "test:e2e-ui": "playwright test --ui", - "test:e2e-ui:debug": "PWDEBUG=1 playwright test --ui", - "test:warnings": "playwright test --grep @warning", - "lint": "eslint \"**/*.{js,ts,jsx,tsx}\" && prettier \"*\" --check --ignore-unknown && tsc", - "lint:errors-only": "eslint \"**/*.{js,ts,jsx,tsx}\" --quiet && prettier \"*\" --check --ignore-unknown && tsc", - "lint:fix": "eslint \"**/*.{js,ts,jsx,tsx}\" --fix && prettier \"*\" --check --write --ignore-unknown && tsc", - "eslint-interactive": "yarn dlx -p eslint -p eslint-interactive eslint-interactive", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build --stats-json", - "browserslist:update": "yarn dlx -p browserslist -p update-browserslist-db update-browserslist-db", - "browserslist:generate": "yarn run browserslist:update && ts-node-esm ./scripts/generateBrowserslistRegex.ts", - "browserslist:dryRun": "yarn run browserslist:generate dryRun" - }, - "browserslist": { - "production": [ - "last 2 chrome version", - "last 2 and_chr version", - "last 2 firefox version", - "last 2 and_ff version", - "last 2 safari version", - "last 2 ios version", - "last 2 edge version" - ], - "vite": [ - "chrome >= 87", - "and_chr >= 87", - "firefox >= 78", - "and_ff >= 78", - "safari >= 14", - "ios >= 14", - "edge >= 88" - ] - }, - "devDependencies": { - "@mdx-js/react": "^3.0.1", - "@mdx-js/rollup": "^3.0.1", - "@playwright/test": "^1.45.1", - "@rest-hooks/test": "^7.3.1", - "@storybook/addon-a11y": "^8.1.10", - "@storybook/addon-actions": "^8.1.10", - "@storybook/addon-essentials": "^8.1.10", - "@storybook/addon-interactions": "^8.1.10", - "@storybook/addon-links": "^8.1.10", - "@storybook/blocks": "^8.1.10", - "@storybook/components": "^8.1.10", - "@storybook/core-events": "^8.1.10", - "@storybook/mdx2-csf": "1.1.0", - "@storybook/react": "^8.1.10", - "@storybook/react-vite": "^8.1.10", - "@storybook/testing-library": "^0.2.2", - "@storybook/theming": "^8.1.10", - "@testing-library/dom": "^10.1.0", - "@testing-library/jest-dom": "^6.4.6", - "@testing-library/react": "^16.0.0", - "@testing-library/user-event": "^14.5.2", - "@types/dompurify": "^3.0.5", - "@types/dotenv-flow": "^3.3.3", - "@types/downloadjs": "^1.4.6", - "@types/github-slugger": "^1.3.0", - "@types/html-to-text": "^9.0.4", - "@types/lodash": "^4.17.6", - "@types/mdx": "^2.0.13", - "@types/node": "^20.12.5", - "@types/react": "18.3.3", - "@types/react-dom": "^18.3.0", - "@types/react-router-dom": "^5.3.3", - "@types/react-scroll-sync": "^0.9.0", - "@types/sanitize-html": "^2.11.0", - "@typescript-eslint/eslint-plugin": "^7.13.1", - "@typescript-eslint/parser": "^7.13.1", - "@vitejs/plugin-react": "^4.3.1", - "@vitest/coverage-istanbul": "^1.6.0", - "@vitest/ui": "^1.6.0", - "autoprefixer": "^10.4.19", - "browserslist": "^4.23.1", - "browserslist-useragent-regexp": "^4.1.3", - "chromatic": "^11.5.4", - "cross-env": "^7.0.3", - "dotenv-flow": "^4.1.0", - "eslint": "8.57", - "eslint-config-prettier": "^9.1.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jest-dom": "^5.4.0", - "eslint-plugin-jsx-a11y": "^6.9.0", - "eslint-plugin-playwright": "^1.6.2", - "eslint-plugin-react": "^7.34.3", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "eslint-plugin-storybook": "^0.8.0", - "eslint-plugin-testing-library": "^6.2.2", - "eslint-plugin-vitest": "^0.5.4", - "husky": "^9.0.11", - "jsdom": "^24.1.0", - "lint-staged": "^15.2.7", - "mockdate": "^3.0.5", - "msw": "^2.3.1", - "msw-storybook-addon": "beta", - "npm-run-all": "^4.1.5", - "otpauth": "^9.3.1", - "patch-package": "^8.0.0", - "postcss": "^8.4.39", - "prettier": "^3.3.2", - "react-error-boundary": "^4.0.13", - "remark-frontmatter": "^5.0.0", - "remark-mdx-frontmatter": "^4.0.0", - "remark-mdx-toc": "^0.3.1", - "sass": "^1.77.6", - "storybook": "^8.1.10", - "storybook-addon-remix-react-router": "^3.0.0", - "ts-node": "^10.9.2", - "tslib": "^2.6.3", - "typescript": "^5.4.5", - "undici": "^6.19.2", - "vite": "^5.3.3", - "vite-plugin-checker": "^0.7.1", - "vite-plugin-svgr": "^4.2.0", - "vitest": "^1.6.0" - }, - "resolutions": { - "@types/react": "18.3.3" - }, - "engines": { - "node": "^20.14" - }, - "packageManager": "yarn@3.6.3" + "name": "react-frontend", + "version": "0.1.1", + "private": true, + "type": "module", + "npmClient": "yarn", + "dependencies": { + "@microsoft/applicationinsights-react-js": "^17.3.0", + "@microsoft/applicationinsights-web": "^3.3.0", + "@okta/okta-react": "^6.9.0", + "@okta/okta-signin-widget": "^7.19.6", + "@rest-hooks/rest": "^3.0.3", + "@tanstack/react-query": "^5.49.2", + "@tanstack/react-query-devtools": "^5.49.2", + "@trussworks/react-uswds": "^9.0.0", + "@uswds/uswds": "3.7.1", + "axios": "^1.7.2", + "classnames": "^2.5.1", + "date-fns": "^3.6.0", + "date-fns-tz": "^3.1.3", + "dompurify": "^3.1.6", + "downloadjs": "^1.4.7", + "export-to-csv-fix-source-map": "^0.2.1", + "focus-trap-react": "^10.2.3", + "history": "^5.3.0", + "html-to-text": "^9.0.5", + "lodash": "^4.17.21", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", + "react-idle-timer": "^5.7.2", + "react-loader-spinner": "^6.1.6", + "react-markdown": "^9.0.1", + "react-query-kit": "^3.3.0", + "react-router": "^6.24.1", + "react-router-dom": "^6.24.1", + "react-scroll-sync": "^0.11.2", + "react-toastify": "^10.0.5", + "rehype-raw": "^7.0.0", + "rehype-slug": "^5.1.0", + "rest-hooks": "^6.1.7", + "sanitize-html": "^2.13.0", + "tsx": "^4.16.2", + "use-deep-compare-effect": "^1.8.1", + "uuid": "^10.0.0", + "web-vitals": "^3.4.0" + }, + "scripts": { + "postinstall": "scripts/postinstall.sh", + "dev": "vite", + "preview": "vite preview --mode preview", + "preview:csp": "yarn run preview --mode csp", + "preview:test": "yarn run preview --mode test", + "preview:ci": "yarn run preview --mode ci", + "preview:build": "yarn run build:production && yarn run preview", + "preview:build:csp": "yarn run build:csp && yarn run preview:csp", + "preview:build:test": "yarn run build:test && yarn run preview:test", + "preview:build:ci": "yarn run build:ci && yarn run preview:ci", + "build:test": "yarn run build-base --mode test", + "build:ci": "yarn run build-base --mode ci", + "build:demo1": "yarn run build-base --mode demo1", + "build:demo2": "yarn run build-base --mode demo2", + "build:demo3": "yarn run build-base --mode demo3", + "build:trialfrontend01": "yarn run build-base --mode trialfrontend01", + "build:trialfrontend02": "yarn run build-base --mode trialfrontend02", + "build:trialfrontend03": "yarn run build-base --mode trialfrontend03", + "build:staging": "yarn run build-base --mode staging", + "build:production": "yarn run build-base", + "build:csp": "yarn run build-base --mode csp", + "build-base": "vite build", + "test": "cross-env vitest", + "test:debug": "cross-env DEBUG_PRINT_LIMIT=100000 vitest --run --no-file-parallelism", + "test:ci": "cross-env VITE_BACKEND_URL=http://localhost vitest --coverage", + "test:ui": "cross-env vitest --ui", + "test:e2e": "playwright test", + "test:e2e-smoke": "MOCK_DISABLED=true playwright test --project chromium --grep @smoke", + "test:e2e-ui": "playwright test --ui", + "test:e2e-ui:smoke": "MOCK_DISABLED=true playwright test --project chromium --grep @smoke --ui", + "test:e2e-ui:debug": "PWDEBUG=1 playwright test --ui", + "test:warnings": "playwright test --grep @warning", + "lint": "eslint \"**/*.{js,ts,jsx,tsx}\" && prettier \"*\" --check --ignore-unknown && tsc", + "lint:errors-only": "eslint \"**/*.{js,ts,jsx,tsx}\" --quiet && prettier \"*\" --check --ignore-unknown && tsc", + "lint:fix": "eslint \"**/*.{js,ts,jsx,tsx}\" --fix && prettier \"*\" --check --write --ignore-unknown && tsc", + "eslint-interactive": "yarn dlx -p eslint -p eslint-interactive eslint-interactive", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build --stats-json", + "browserslist:update": "yarn dlx -p browserslist -p update-browserslist-db update-browserslist-db", + "browserslist:generate": "yarn run browserslist:update && ts-node-esm ./scripts/generateBrowserslistRegex.ts", + "browserslist:dryRun": "yarn run browserslist:generate dryRun" + }, + "browserslist": { + "production": [ + "last 2 chrome version", + "last 2 and_chr version", + "last 2 firefox version", + "last 2 and_ff version", + "last 2 safari version", + "last 2 ios version", + "last 2 edge version" + ], + "vite": [ + "chrome >= 87", + "and_chr >= 87", + "firefox >= 78", + "and_ff >= 78", + "safari >= 14", + "ios >= 14", + "edge >= 88" + ] + }, + "devDependencies": { + "@mdx-js/react": "^3.0.1", + "@mdx-js/rollup": "^3.0.1", + "@playwright/test": "^1.45.1", + "@rest-hooks/test": "^7.3.1", + "@storybook/addon-a11y": "^8.1.10", + "@storybook/addon-actions": "^8.1.10", + "@storybook/addon-essentials": "^8.1.10", + "@storybook/addon-interactions": "^8.1.10", + "@storybook/addon-links": "^8.1.10", + "@storybook/blocks": "^8.1.10", + "@storybook/components": "^8.1.10", + "@storybook/core-events": "^8.1.10", + "@storybook/mdx2-csf": "1.1.0", + "@storybook/react": "^8.1.10", + "@storybook/react-vite": "^8.1.10", + "@storybook/testing-library": "^0.2.2", + "@storybook/theming": "^8.1.10", + "@testing-library/dom": "^10.1.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", + "@testing-library/user-event": "^14.5.2", + "@types/dompurify": "^3.0.5", + "@types/dotenv-flow": "^3.3.3", + "@types/downloadjs": "^1.4.6", + "@types/github-slugger": "^1.3.0", + "@types/html-to-text": "^9.0.4", + "@types/lodash": "^4.17.6", + "@types/mdx": "^2.0.13", + "@types/node": "^20.12.5", + "@types/react": "18.3.3", + "@types/react-dom": "^18.3.0", + "@types/react-router-dom": "^5.3.3", + "@types/react-scroll-sync": "^0.9.0", + "@types/sanitize-html": "^2.11.0", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "@vitejs/plugin-react": "^4.3.1", + "@vitest/coverage-istanbul": "^1.6.0", + "@vitest/ui": "^1.6.0", + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.1", + "browserslist-useragent-regexp": "^4.1.3", + "chromatic": "^11.5.4", + "cross-env": "^7.0.3", + "dotenv-flow": "^4.1.0", + "eslint": "8.57", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jest-dom": "^5.4.0", + "eslint-plugin-jsx-a11y": "^6.9.0", + "eslint-plugin-playwright": "^1.6.2", + "eslint-plugin-react": "^7.34.3", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "eslint-plugin-storybook": "^0.8.0", + "eslint-plugin-testing-library": "^6.2.2", + "eslint-plugin-vitest": "^0.5.4", + "husky": "^9.0.11", + "jsdom": "^24.1.0", + "lint-staged": "^15.2.7", + "mockdate": "^3.0.5", + "msw": "^2.3.1", + "msw-storybook-addon": "beta", + "npm-run-all": "^4.1.5", + "otpauth": "^9.3.1", + "patch-package": "^8.0.0", + "postcss": "^8.4.39", + "prettier": "^3.3.2", + "react-error-boundary": "^4.0.13", + "remark-frontmatter": "^5.0.0", + "remark-mdx-frontmatter": "^4.0.0", + "remark-mdx-toc": "^0.3.1", + "sass": "^1.77.6", + "storybook": "^8.1.10", + "storybook-addon-remix-react-router": "^3.0.0", + "ts-node": "^10.9.2", + "tslib": "^2.6.3", + "typescript": "^5.4.5", + "undici": "^6.19.2", + "vite": "^5.3.3", + "vite-plugin-checker": "^0.7.1", + "vite-plugin-svgr": "^4.2.0", + "vitest": "^1.6.0" + }, + "resolutions": { + "@types/react": "18.3.3" + }, + "engines": { + "node": "^20.14" + }, + "packageManager": "yarn@3.6.3" } diff --git a/frontend-react/playwright.config.ts b/frontend-react/playwright.config.ts index eb32f50c4bd..79ff5540a92 100644 --- a/frontend-react/playwright.config.ts +++ b/frontend-react/playwright.config.ts @@ -23,9 +23,7 @@ export default defineConfig({ // Do not consume 100% cpu, as this will cause instability workers: isCi ? "75%" : undefined, // Tests sharded in CI runner and reported as blobs that are later turned into html report - reporter: isCi - ? [["blob", { outputDir: "e2e-data/report" }]] - : [["html", { outputFolder: "e2e-data/report" }]], + reporter: isCi ? [["blob", { outputDir: "e2e-data/report" }]] : [["html", { outputFolder: "e2e-data/report" }]], outputDir: "e2e-data/results", use: { // keep playwright and browser timezones aligned. set preferably UTC by env var diff --git a/frontend-react/src/content/support/index.mdx b/frontend-react/src/content/support/index.mdx index 1cf2efe6544..6af0709fae8 100644 --- a/frontend-react/src/content/support/index.mdx +++ b/frontend-react/src/content/support/index.mdx @@ -203,7 +203,7 @@ import site from "../../content/site.json";

- View specific reporting requirements for COVID-19 + View specific reporting requirements for COVID-19 {" "} and for mpox.

) diff --git a/frontend-react/tsconfig.json b/frontend-react/tsconfig.json index e9a61ec06cd..4d587b83318 100644 --- a/frontend-react/tsconfig.json +++ b/frontend-react/tsconfig.json @@ -1,29 +1,27 @@ { - "compilerOptions": { - "target": "ESNext", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "noFallthroughCasesInSwitch": true, - "module": "Preserve", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "types": ["vitest/globals", "mdx", "vite/client"], - "noImplicitAny": true, - "inlineSourceMap": true, - "paths": { - "@trussworks/react-uswds/lib/*": [ - "./node_modules/@trussworks/react-uswds/lib/*" - ] - } - }, - "include": ["./src", "./e2e", "./__mocks__", "playwright.config.ts"], - "references": [{ "path": "./tsconfig.node.json" }] + "compilerOptions": { + "target": "ESNext", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "Preserve", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": ["vitest/globals", "mdx", "vite/client"], + "noImplicitAny": true, + "inlineSourceMap": true, + "paths": { + "@trussworks/react-uswds/lib/*": ["./node_modules/@trussworks/react-uswds/lib/*"] + } + }, + "include": ["./src", "./e2e", "./__mocks__", "playwright.config.ts"], + "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/frontend-react/tsconfig.node.json b/frontend-react/tsconfig.node.json index af7c06e3046..cfa1ab5b950 100644 --- a/frontend-react/tsconfig.node.json +++ b/frontend-react/tsconfig.node.json @@ -1,10 +1,10 @@ { - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] } diff --git a/frontend-react/unsupported-browser.html b/frontend-react/unsupported-browser.html index 10974ef2a5a..a4dc879f25d 100644 --- a/frontend-react/unsupported-browser.html +++ b/frontend-react/unsupported-browser.html @@ -6,30 +6,17 @@ - + ReportStream - Unsupported Browser
-
+
-
-
+
+

- An official website of the United States - government -

- +
-