From add4ddccb1f6e83932a3db853bf7ed400f4975a7 Mon Sep 17 00:00:00 2001
From: Jason Lin <98117700+JasonLin0991@users.noreply.github.com>
Date: Fri, 26 Jan 2024 16:57:04 -0500
Subject: [PATCH 01/24] MCR-3758: Update launch darkly and fix testing (#2207)
* Update `launchdarkly-react-client-sdk`
* Implement LDProvider to renderWithProviders and update tests.
* Update document on new testing approach.
* Fix react key prop error.
* Use ldClient configuration instead of bootstrap and update docs.
* Cleanup code and fix docs wording.
* Update code snippet
---
.../launch-darkly-testing-approach.md | 122 ++++++++++++------
services/app-web/package.json | 2 +-
.../ContactsSummarySection.tsx | 1 +
.../ContractDetailsSummarySection.test.tsx | 40 +++---
.../SingleRateSummarySection.test.tsx | 19 ++-
.../app-web/src/pages/App/AppBody.test.tsx | 36 +++---
.../app-web/src/pages/App/AppRoutes.test.tsx | 25 ++--
.../src/pages/Landing/Landing.test.tsx | 13 +-
.../QuestionResponse.test.tsx | 33 +++--
.../UploadQuestions/UploadQuestions.test.tsx | 29 +++--
.../UploadResponse/UploadResponse.test.tsx | 26 ++--
.../ContractDetails/ContractDetails.test.tsx | 35 ++---
.../SubmissionSideNav.test.tsx | 18 ++-
.../RateSummary/RateSummary.test.tsx | 49 +++----
.../SubmissionSummary.test.tsx | 11 +-
services/app-web/src/testHelpers/index.ts | 1 -
.../app-web/src/testHelpers/jestHelpers.tsx | 114 ++++++++--------
yarn.lock | 19 ++-
18 files changed, 339 insertions(+), 254 deletions(-)
diff --git a/docs/technical-design/launch-darkly-testing-approach.md b/docs/technical-design/launch-darkly-testing-approach.md
index f697fb47ed..abd1c8a423 100644
--- a/docs/technical-design/launch-darkly-testing-approach.md
+++ b/docs/technical-design/launch-darkly-testing-approach.md
@@ -23,60 +23,98 @@ export LD_SDK_KEY='Place Launch Darkly SDK key here'
## Feature flag unit testing
### Client side unit testing
-Client side unit testing utilizes `jest.spyOn()` to mock the LaunchDarkly `useLDClient` hook and return default flag values or values specified. This implementation is done in our jest helper function `ldUseClientSpy()` located in `app-web/src/testHelpers/jestHelpers.tsx`.
+Client side unit testing utilizes the `LDProvider`, from `launchdarkly-react-client-sdk`, in our [renderWithProviders](../../services/app-web/src/testHelpers/jestHelpers.tsx) function to set up feature flags for each of the tests. Currently, LaunchDarkly does not provide an actual mock `LDProvider` so if or when they do, then we could update this with that provider.
-`ldUseClientSpy` takes in an object of feature flags and values as an argument. You can configure multiple flags with the object passed into `ldUseClientSpy`.
+We use this method for testing because the official documented [unit testing](https://docs.launchdarkly.com/guides/sdk/unit-tests/?q=unit+test) method by LaunchDarkly does not work with our LaunchDarkly implementation. Our implementation follow exactly the documentation, so it could be how we are setting up our unit tests. Previously we had used `jest.spyOn` to intercept `useLDClient` and mock the `useLDClient.variation()` function with our defined feature flag values. With `launchdarkly-react-client-sdk@3.0.10` that method did not work anymore.
-```javascript
-ldUseClientSpy({
- 'rates-across-submissions': true,
- 'rate-cert-assurance': true,
-})
-```
+#### Configuration
+When using the `LDProvider` we need to pass in a mocked `ldClient` in the configuration. This allows us to initialize `ldClient` outside of the provider, which would have required the provider to perform an API call to LaunchDarkly. Now that this API call does not happen it isolates our unit tests from the feature flag values on the LaunchDarkly server and only use the values we define in each test.
-To configure feature flags for a single test place `ldUseClientSpy` at the beginning of your test.
+The configuration below, in `renderWithProviders`, the `ldClient` field is how we initialize `ldClient` with our defined flag values. We are using the `ldClientMock()` function to generate a mock that matches the type this field requires.
-```javascript
-it('cannot continue if no documents are added to the second rate', async () => {
- ldUseClientSpy({ 'rates-across-submissions': true })
- const mockUpdateDraftFn = jest.fn()
- renderWithProviders(
- ,
- {
- apolloProvider: {
- mocks: [fetchCurrentUserMock({ statusCode: 200 })],
- },
- }
- )
+You will also see that, compared to our configuration in [app-web/src/index.tsx](../../services/app-web/src/index.tsx), the config needed to connect to LaunchDarkly is replaced with `test-url`.
- ...
-})
+```typescript
+const ldProviderConfig: ProviderConfig = {
+ clientSideID: 'test-url',
+ options: {
+ bootstrap: flags,
+ baseUrl: 'test-url',
+ streamUrl: 'test-url',
+ eventsUrl: 'test-url',
+ },
+ ldClient: ldClientMock(flags)
+}
```
-To configure multiple tests inside a `describe` block you can:
-- Follow the method for single test on each test inside the `describe`.
-- If all the tests require the same flag configuration place `ldUseClientSpy` at the top of the block in `beforeEach()`.
+The two important functions in the `ldCientMock` are `variation` and `allFlags`. These two functions are the ones we use in the app to get feature flags and here we are mocking them with the flag values we define in each test. If we need any other functions in `ldClient` we would just add the mock to `ldClientMock()`.
```javascript
-describe('rates across submissions', () => {
- beforeEach(() =>
- ldUseClientSpy({
- 'rates-across-submissions': true,
- })
- )
- afterEach(() => {
- jest.clearAllMocks()
- })
-
- ...
+const ldClientMock = (featureFlags: FeatureFlagSettings): LDClient => ({
+ ... other functions,
+ variation: jest.fn(
+ (
+ flag: FeatureFlagLDConstant,
+ defaultValue: FlagValue | undefined
+ ) => featureFlags[flag] ?? defaultValue
+ ),
+ allFlags: jest.fn(() => featureFlags),
})
```
-It's always best to `jest.clearAllMocks()` after each test with either one of these methods, otherwise, preceding tests may have the same flag configured as the previous test.
+We define our initial feature flag values in the `flags` variable by combining the default feature flag values with values passed into `renderWithProviders` for each test. Looking at the code snippet below from `renderWithProviders`, we get the default flag values from [flags.ts](../../services/app-web/src/common-code/featureFlags/flags.ts) using `getDefaultFeatureFlags()` then merge that with `option.featureFlags` values passed into `renderWithProviders`. This will allow each test to configure the specific feature flag values for that test and supply default values for flags the test did not define.
+
+```typescript
+const {
+ routerProvider = {},
+ apolloProvider = {},
+ authProvider = {},
+ s3Provider = undefined,
+ location = undefined,
+ featureFlags = undefined
+} = options || {}
+
+const flags = {
+ ...getDefaultFeatureFlags(),
+ ...featureFlags
+}
+
+const ldProviderConfig: ProviderConfig = {
+ clientSideID: 'test-url',
+ options: {
+ bootstrap: flags,
+ baseUrl: 'test-url',
+ streamUrl: 'test-url',
+ eventsUrl: 'test-url',
+ },
+ ldClient: ldClientMock(flags)
+}
+```
+
+#### Examples
+
+Using this method in our unit tests is simple and similar to how we configure the other providers. When calling `renderWithProdivers` we need to supply the second argument `options` with the `featureFlag` field.
+
+In the example below we set `featureFlag` with an object that contains two feature flags and their values. When this test is run, the component will be supplied with these two flag values along with the other default flag values from [flags.ts](../../services/app-web/src/common-code/featureFlags/flags.ts). Take note that the `featureFlag` field is type `FeatureFlagSettings` so you will only be allowed to define flags that exists in [flags.ts](../../services/app-web/src/common-code/featureFlags/flags.ts).
+
+```javascript
+renderWithProviders(
+ ,
+ {
+ apolloProvider: {
+ mocks: [fetchCurrentUserMock({ statusCode: 200 })],
+ },
+ featureFlags: {
+ 'rate-edit-unlock': false,
+ '438-attestation': true
+ }
+ }
+)
+```
### Server side unit testing
LaunchDarkly server side implementation is done by configuring our resolvers with `ldService` dependency. In our resolver we then can use the method `getFeatureFlag` from `ldService` to get the flag value from LaunchDarkly.
diff --git a/services/app-web/package.json b/services/app-web/package.json
index b0f313b8b2..a4299ec662 100644
--- a/services/app-web/package.json
+++ b/services/app-web/package.json
@@ -105,7 +105,7 @@
"graphql": "^16.2.0",
"jotai": "^2.2.1",
"jotai-location": "^0.5.1",
- "launchdarkly-react-client-sdk": "^3.0.1",
+ "launchdarkly-react-client-sdk": "^3.0.10",
"path-browserify": "^1.0.1",
"qs": "^6.11.0",
"react": "^18.2.0",
diff --git a/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx b/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx
index 8a721a18bd..05a3cb7714 100644
--- a/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx
+++ b/services/app-web/src/components/SubmissionSummarySection/ContactsSummarySection/ContactsSummarySection.tsx
@@ -51,6 +51,7 @@ export const ContactsSummarySection = ({
submission.stateContacts.map(
(stateContact, index) => (
{
- afterEach(() => {
- jest.restoreAllMocks()
- })
const defaultApolloMocks = {
mocks: [fetchCurrentUserMock({ statusCode: 200 })],
}
@@ -110,8 +104,7 @@ describe('ContractDetailsSummarySection', () => {
})
})
- it('can render all contract details fields', () => {
- ldUseClientSpy({ '438-attestation': true })
+ it('can render all contract details fields', async () => {
const submission = mockContractAndRatesDraft({
statutoryRegulatoryAttestation: true,
})
@@ -124,14 +117,18 @@ describe('ContractDetailsSummarySection', () => {
/>,
{
apolloProvider: defaultApolloMocks,
+ featureFlags: { '438-attestation': true },
}
)
- expect(
- screen.getByRole('definition', {
- name: StatutoryRegulatoryAttestationQuestion,
- })
- ).toBeInTheDocument()
+ await waitFor(() => {
+ expect(
+ screen.getByRole('definition', {
+ name: StatutoryRegulatoryAttestationQuestion,
+ })
+ ).toBeInTheDocument()
+ })
+
expect(
screen.getByRole('definition', { name: 'Contract status' })
).toBeInTheDocument()
@@ -161,7 +158,6 @@ describe('ContractDetailsSummarySection', () => {
})
it('displays correct contract 438 attestation yes and no text and description', async () => {
- ldUseClientSpy({ '438-attestation': true })
const submission = mockContractAndRatesDraft({
statutoryRegulatoryAttestation: false,
statutoryRegulatoryAttestationDescription: 'No compliance',
@@ -175,14 +171,18 @@ describe('ContractDetailsSummarySection', () => {
/>,
{
apolloProvider: defaultApolloMocks,
+ featureFlags: { '438-attestation': true },
}
)
- expect(
- screen.getByRole('definition', {
- name: StatutoryRegulatoryAttestationQuestion,
- })
- ).toBeInTheDocument()
+ await waitFor(() => {
+ expect(
+ screen.getByRole('definition', {
+ name: StatutoryRegulatoryAttestationQuestion,
+ })
+ ).toBeInTheDocument()
+ })
+
expect(
screen.getByRole('definition', {
name: 'Non-compliance description',
diff --git a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection.test.tsx b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection.test.tsx
index 5ad5218f7b..15697941ab 100644
--- a/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection.test.tsx
+++ b/services/app-web/src/components/SubmissionSummarySection/RateDetailsSummarySection/SingleRateSummarySection.test.tsx
@@ -1,7 +1,4 @@
-import {
- ldUseClientSpy,
- renderWithProviders,
-} from '../../../testHelpers/jestHelpers'
+import { renderWithProviders } from '../../../testHelpers/jestHelpers'
import { SingleRateSummarySection } from './SingleRateSummarySection'
import {
fetchCurrentUserMock,
@@ -14,13 +11,6 @@ import { packageName } from '../../../common-code/healthPlanFormDataType'
import { RateRevision } from '../../../gen/gqlClient'
describe('SingleRateSummarySection', () => {
- beforeEach(() => {
- ldUseClientSpy({ 'rate-edit-unlock': true })
- })
- afterEach(() => {
- jest.resetAllMocks()
- })
-
it('can render rate details without errors', async () => {
const rateData = rateDataMock()
await waitFor(() => {
@@ -39,6 +29,7 @@ describe('SingleRateSummarySection', () => {
}),
],
},
+ featureFlags: { 'rate-edit-unlock': true },
}
)
})
@@ -134,6 +125,7 @@ describe('SingleRateSummarySection', () => {
}),
],
},
+ featureFlags: { 'rate-edit-unlock': true },
}
)
})
@@ -234,6 +226,7 @@ describe('SingleRateSummarySection', () => {
}),
],
},
+ featureFlags: { 'rate-edit-unlock': true },
}
)
@@ -273,6 +266,7 @@ describe('SingleRateSummarySection', () => {
}),
],
},
+ featureFlags: { 'rate-edit-unlock': true },
}
)
@@ -313,6 +307,7 @@ describe('SingleRateSummarySection', () => {
}),
],
},
+ featureFlags: { 'rate-edit-unlock': true },
}
)
expect(
@@ -345,6 +340,7 @@ describe('SingleRateSummarySection', () => {
}),
],
},
+ featureFlags: { 'rate-edit-unlock': true },
}
)
await waitFor(() => {
@@ -379,6 +375,7 @@ describe('SingleRateSummarySection', () => {
}),
],
},
+ featureFlags: { 'rate-edit-unlock': true },
}
)
expect(
diff --git a/services/app-web/src/pages/App/AppBody.test.tsx b/services/app-web/src/pages/App/AppBody.test.tsx
index 9968b35577..2947038058 100644
--- a/services/app-web/src/pages/App/AppBody.test.tsx
+++ b/services/app-web/src/pages/App/AppBody.test.tsx
@@ -1,7 +1,6 @@
import { screen } from '@testing-library/react'
import {
- ldUseClientSpy,
renderWithProviders,
userClickSignIn,
} from '../../testHelpers/jestHelpers'
@@ -26,16 +25,18 @@ describe('AppBody', () => {
})
it('App renders without errors', () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
- renderWithProviders()
+ renderWithProviders(, {
+ featureFlags: { 'session-expiring-modal': false },
+ })
const mainElement = screen.getByRole('main')
expect(mainElement).toBeInTheDocument()
})
describe('Sign In buttton click', () => {
it('displays local login heading when expected', async () => {
- ldUseClientSpy({})
- renderWithProviders()
+ renderWithProviders(, {
+ featureFlags: { 'session-expiring-modal': false },
+ })
await userClickSignIn(screen)
@@ -48,8 +49,9 @@ describe('AppBody', () => {
})
it('displays Cognito login page when expected', async () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
- renderWithProviders()
+ renderWithProviders(, {
+ featureFlags: { 'session-expiring-modal': false },
+ })
await userClickSignIn(screen)
expect(
@@ -58,8 +60,9 @@ describe('AppBody', () => {
})
it('displays Cognito signup page when expected', async () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
- renderWithProviders()
+ renderWithProviders(, {
+ featureFlags: { 'session-expiring-modal': false },
+ })
await userClickSignIn(screen)
expect(
@@ -88,7 +91,6 @@ describe('AppBody', () => {
it('shows test environment banner in val', () => {
process.env.REACT_APP_STAGE_NAME = 'val'
- ldUseClientSpy({ 'session-expiring-modal': false })
renderWithProviders(, {
apolloProvider: {
mocks: [
@@ -96,6 +98,7 @@ describe('AppBody', () => {
indexHealthPlanPackagesMockSuccess(),
],
},
+ featureFlags: { 'session-expiring-modal': false },
})
expect(
@@ -105,7 +108,6 @@ describe('AppBody', () => {
it('does not show test environment banner in prod', () => {
process.env.REACT_APP_STAGE_NAME = 'prod'
- ldUseClientSpy({ 'session-expiring-modal': false })
renderWithProviders(, {
apolloProvider: {
mocks: [
@@ -113,6 +115,7 @@ describe('AppBody', () => {
indexHealthPlanPackagesMockSuccess(),
],
},
+ featureFlags: { 'session-expiring-modal': false },
})
expect(screen.queryByText('THIS IS A TEST ENVIRONMENT')).toBeNull()
@@ -121,10 +124,6 @@ describe('AppBody', () => {
describe('Site under maintenance banner', () => {
it('displays maintenance banner when feature flag is on', async () => {
- ldUseClientSpy({
- 'site-under-maintenance-banner': 'UNSCHEDULED',
- 'session-expiring-modal': false,
- })
renderWithProviders(, {
apolloProvider: {
mocks: [
@@ -132,6 +131,10 @@ describe('AppBody', () => {
indexHealthPlanPackagesMockSuccess(),
],
},
+ featureFlags: {
+ 'site-under-maintenance-banner': 'UNSCHEDULED',
+ 'session-expiring-modal': false,
+ },
})
expect(
await screen.findByRole('heading', { name: 'Site unavailable' })
@@ -144,7 +147,6 @@ describe('AppBody', () => {
})
it('does not display maintenance banner when flag is off', async () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
renderWithProviders(, {
apolloProvider: {
mocks: [
@@ -152,6 +154,7 @@ describe('AppBody', () => {
indexHealthPlanPackagesMockSuccess(),
],
},
+ featureFlags: { 'session-expiring-modal': false },
})
expect(
screen.queryByRole('heading', { name: 'Site Unavailable' })
@@ -166,7 +169,6 @@ describe('AppBody', () => {
describe('Page scrolling', () => {
it('scroll top on page load', async () => {
- ldUseClientSpy({})
renderWithProviders()
await userClickSignIn(screen)
expect(window.scrollTo).toHaveBeenCalledWith(0, 0)
diff --git a/services/app-web/src/pages/App/AppRoutes.test.tsx b/services/app-web/src/pages/App/AppRoutes.test.tsx
index e0bc56c927..a9fd159262 100644
--- a/services/app-web/src/pages/App/AppRoutes.test.tsx
+++ b/services/app-web/src/pages/App/AppRoutes.test.tsx
@@ -1,9 +1,6 @@
import { screen, waitFor } from '@testing-library/react'
-import {
- ldUseClientSpy,
- renderWithProviders,
-} from '../../testHelpers/jestHelpers'
+import { renderWithProviders } from '../../testHelpers/jestHelpers'
import { AppRoutes } from './AppRoutes'
import {
fetchCurrentUserMock,
@@ -22,7 +19,6 @@ describe('AppRoutes', () => {
})
describe('/[root]', () => {
it('state dashboard when state user logged in', async () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
renderWithProviders(, {
apolloProvider: {
mocks: [
@@ -30,6 +26,7 @@ describe('AppRoutes', () => {
indexHealthPlanPackagesMockSuccess(),
],
},
+ featureFlags: { 'session-expiring-modal': false },
})
await waitFor(() => {
@@ -46,7 +43,6 @@ describe('AppRoutes', () => {
})
it('cms dashboard when cms user logged in', async () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
renderWithProviders(, {
apolloProvider: {
mocks: [
@@ -57,6 +53,7 @@ describe('AppRoutes', () => {
indexHealthPlanPackagesMockSuccess(),
],
},
+ featureFlags: { 'session-expiring-modal': false },
})
await waitFor(() => {
@@ -73,7 +70,6 @@ describe('AppRoutes', () => {
})
it('landing page when no user', async () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
renderWithProviders(, {
apolloProvider: {
mocks: [
@@ -82,6 +78,7 @@ describe('AppRoutes', () => {
}),
],
},
+ featureFlags: { 'session-expiring-modal': false },
})
await waitFor(() => {
expect(
@@ -102,7 +99,6 @@ describe('AppRoutes', () => {
describe('/auth', () => {
it('auth header is displayed', async () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
renderWithProviders(, {
routerProvider: { route: '/auth' },
apolloProvider: {
@@ -112,6 +108,7 @@ describe('AppRoutes', () => {
}),
],
},
+ featureFlags: { 'session-expiring-modal': false },
})
await waitFor(() => {
@@ -136,8 +133,13 @@ describe('AppRoutes', () => {
renderWithProviders(, {
routerProvider: { route: '/help' },
apolloProvider: {
- mocks: [fetchCurrentUserMock({ statusCode: 200 })],
+ mocks: [
+ fetchCurrentUserMock({
+ statusCode: 200,
+ }),
+ ],
},
+ featureFlags: { 'session-expiring-modal': false },
})
await screen.findByTestId('help-authenticated')
@@ -162,6 +164,7 @@ describe('AppRoutes', () => {
}),
],
},
+ featureFlags: { 'session-expiring-modal': false },
})
await screen.findByTestId('help-authenticated')
await waitFor(() => {
@@ -200,7 +203,6 @@ describe('AppRoutes', () => {
describe('invalid routes', () => {
it('redirect to landing page when no user', async () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
renderWithProviders(, {
routerProvider: { route: '/not-a-real-place' },
apolloProvider: {
@@ -210,6 +212,7 @@ describe('AppRoutes', () => {
}),
],
},
+ featureFlags: { 'session-expiring-modal': false },
})
await waitFor(() => {
@@ -223,12 +226,12 @@ describe('AppRoutes', () => {
})
it('redirect to 404 error page when user is logged in', async () => {
- ldUseClientSpy({ 'session-expiring-modal': false })
renderWithProviders(, {
apolloProvider: {
mocks: [fetchCurrentUserMock({ statusCode: 200 })],
},
routerProvider: { route: '/not-a-real-place' },
+ featureFlags: { 'session-expiring-modal': false },
})
await waitFor(() =>
diff --git a/services/app-web/src/pages/Landing/Landing.test.tsx b/services/app-web/src/pages/Landing/Landing.test.tsx
index 46aa0d635f..11fbbf34da 100644
--- a/services/app-web/src/pages/Landing/Landing.test.tsx
+++ b/services/app-web/src/pages/Landing/Landing.test.tsx
@@ -1,17 +1,16 @@
import { screen } from '@testing-library/react'
-import {
- ldUseClientSpy,
- renderWithProviders,
-} from '../../testHelpers/jestHelpers'
+import { renderWithProviders } from '../../testHelpers/jestHelpers'
import { Landing } from './Landing'
describe('Landing', () => {
afterAll(() => jest.clearAllMocks())
it('displays session expired when query parameter included', async () => {
- ldUseClientSpy({ 'site-under-maintenance-banner': false })
renderWithProviders(, {
routerProvider: { route: '/?session-timeout' },
+ featureFlags: {
+ 'site-under-maintenance-banner': false,
+ },
})
expect(
screen.queryByRole('heading', { name: 'Session expired' })
@@ -21,9 +20,11 @@ describe('Landing', () => {
).toBeNull()
})
it('does not display session expired by default', async () => {
- ldUseClientSpy({ 'site-under-maintenance-banner': false })
renderWithProviders(, {
routerProvider: { route: '/' },
+ featureFlags: {
+ 'site-under-maintenance-banner': false,
+ },
})
expect(
screen.queryByRole('heading', {
diff --git a/services/app-web/src/pages/QuestionResponse/QuestionResponse.test.tsx b/services/app-web/src/pages/QuestionResponse/QuestionResponse.test.tsx
index aa3ebb5de6..39ce59d492 100644
--- a/services/app-web/src/pages/QuestionResponse/QuestionResponse.test.tsx
+++ b/services/app-web/src/pages/QuestionResponse/QuestionResponse.test.tsx
@@ -2,7 +2,7 @@ import { screen, waitFor, within } from '@testing-library/react'
import { Route, Routes } from 'react-router-dom'
import { SubmissionSideNav } from '../SubmissionSideNav'
import { QuestionResponse } from './QuestionResponse'
-import { ldUseClientSpy, renderWithProviders } from '../../testHelpers'
+import { renderWithProviders } from '../../testHelpers'
import { RoutesRecord } from '../../constants/routes'
import {
@@ -15,13 +15,6 @@ import {
import { IndexQuestionsPayload } from '../../gen/gqlClient'
describe('QuestionResponse', () => {
- beforeEach(() => {
- ldUseClientSpy({ 'cms-questions': true })
- })
- afterEach(() => {
- jest.resetAllMocks()
- })
-
it('renders expected questions correctly with rounds', async () => {
const mockQuestions = mockQuestionsPayload('15')
@@ -50,6 +43,9 @@ describe('QuestionResponse', () => {
routerProvider: {
route: '/submissions/15/question-and-answers',
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -177,6 +173,9 @@ describe('QuestionResponse', () => {
routerProvider: {
route: '/submissions/15/question-and-answers',
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -231,6 +230,9 @@ describe('QuestionResponse', () => {
routerProvider: {
route: '/submissions/15/question-and-answers',
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -291,6 +293,9 @@ describe('QuestionResponse', () => {
routerProvider: {
route: '/submissions/15/question-and-answers',
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -338,6 +343,9 @@ describe('QuestionResponse', () => {
routerProvider: {
route: '/submissions/15/question-and-answers?submit=question',
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -372,6 +380,9 @@ describe('QuestionResponse', () => {
routerProvider: {
route: '/submissions/15/question-and-answers?submit=response',
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -404,6 +415,9 @@ describe('QuestionResponse', () => {
routerProvider: {
route: '/submissions/15/question-and-answers',
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -438,6 +452,9 @@ describe('QuestionResponse', () => {
routerProvider: {
route: '/submissions/15/question-and-answers',
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
diff --git a/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.test.tsx b/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.test.tsx
index ad388fe0b5..07311432ad 100644
--- a/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.test.tsx
+++ b/services/app-web/src/pages/QuestionResponse/UploadQuestions/UploadQuestions.test.tsx
@@ -4,7 +4,6 @@ import { Route, Routes } from 'react-router-dom'
import { UploadQuestions } from '../../QuestionResponse'
import {
dragAndDrop,
- ldUseClientSpy,
renderWithProviders,
TEST_DOC_FILE,
TEST_PDF_FILE,
@@ -26,13 +25,6 @@ import { SubmissionSideNav } from '../../SubmissionSideNav'
import { Location } from 'react-router-dom'
describe('UploadQuestions', () => {
- beforeEach(() => {
- ldUseClientSpy({ 'cms-questions': true })
- })
- afterEach(() => {
- jest.resetAllMocks()
- })
-
it('displays file upload for correct cms division', async () => {
const division = 'testDivision'
renderWithProviders(
@@ -59,6 +51,9 @@ describe('UploadQuestions', () => {
routerProvider: {
route: `/submissions/15/question-and-answers/${division}/upload-questions`,
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -104,6 +99,9 @@ describe('UploadQuestions', () => {
routerProvider: {
route: `/submissions/15/question-and-answers/dmco/upload-questions`,
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -165,6 +163,9 @@ describe('UploadQuestions', () => {
route: `/submissions/15/question-and-answers/dmco/upload-questions`,
},
location: (location) => (testLocation = location),
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -220,6 +221,9 @@ describe('UploadQuestions', () => {
}),
],
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
await screen.findByRole('heading', {
@@ -265,6 +269,9 @@ describe('UploadQuestions', () => {
}),
],
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
await screen.findByRole('heading', {
@@ -315,6 +322,9 @@ describe('UploadQuestions', () => {
}),
],
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
await screen.findByRole('heading', {
@@ -385,6 +395,9 @@ describe('UploadQuestions', () => {
}),
],
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
diff --git a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.test.tsx b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.test.tsx
index de8aab8017..106b143a15 100644
--- a/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.test.tsx
+++ b/services/app-web/src/pages/QuestionResponse/UploadResponse/UploadResponse.test.tsx
@@ -4,7 +4,6 @@ import { Route, Routes } from 'react-router-dom'
import { UploadResponse } from './UploadResponse'
import {
dragAndDrop,
- ldUseClientSpy,
renderWithProviders,
TEST_DOC_FILE,
TEST_PDF_FILE,
@@ -24,13 +23,6 @@ import {
import { SubmissionSideNav } from '../../SubmissionSideNav'
describe('UploadResponse', () => {
- beforeEach(() => {
- ldUseClientSpy({ 'cms-questions': true })
- })
- afterEach(() => {
- jest.resetAllMocks()
- })
-
const division = 'testDivision'
const questionID = 'testQuestion'
@@ -59,6 +51,9 @@ describe('UploadResponse', () => {
routerProvider: {
route: `/submissions/15/question-and-answers/${division}/${questionID}/upload-response`,
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -98,6 +93,9 @@ describe('UploadResponse', () => {
routerProvider: {
route: `/submissions/15/question-and-answers/dmco/${questionID}/upload-response`,
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -145,6 +143,9 @@ describe('UploadResponse', () => {
routerProvider: {
route: `/submissions/15/question-and-answers/dmco/${questionID}/upload-response`,
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
@@ -191,6 +192,9 @@ describe('UploadResponse', () => {
routerProvider: {
route: `/submissions/15/question-and-answers/dmco/${questionID}/upload-response`,
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
await screen.findByRole('heading', {
@@ -241,6 +245,9 @@ describe('UploadResponse', () => {
routerProvider: {
route: `/submissions/15/question-and-answers/dmco/${questionID}/upload-response`,
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
await screen.findByRole('heading', {
@@ -303,6 +310,9 @@ describe('UploadResponse', () => {
routerProvider: {
route: `/submissions/15/question-and-answers/dmco/${questionID}/upload-response`,
},
+ featureFlags: {
+ 'cms-questions': true,
+ },
}
)
await screen.findByRole('heading', {
diff --git a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx
index c12422ab70..fce5b2a833 100644
--- a/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx
+++ b/services/app-web/src/pages/StateSubmission/ContractDetails/ContractDetails.test.tsx
@@ -17,7 +17,6 @@ import {
TEST_PNG_FILE,
dragAndDrop,
selectYesNoRadio,
- ldUseClientSpy,
} from '../../../testHelpers/jestHelpers'
import { ACCEPTED_SUBMISSION_FILE_TYPES } from '../../../components/FileUpload'
import { ContractDetails } from './'
@@ -38,10 +37,6 @@ const scrollIntoViewMock = jest.fn()
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock
describe('ContractDetails', () => {
- afterEach(() => {
- jest.clearAllMocks()
- })
-
const emptyContractDetailsDraft = {
...mockDraft(),
}
@@ -1028,7 +1023,6 @@ describe('ContractDetails', () => {
describe('Contract 438 attestation', () => {
it('renders 438 attestation question without errors', async () => {
- ldUseClientSpy({ '438-attestation': true })
const draft = mockBaseContract({
statutoryRegulatoryAttestation: true,
})
@@ -1041,14 +1035,17 @@ describe('ContractDetails', () => {
/>,
{
apolloProvider: defaultApolloProvider,
+ featureFlags: { '438-attestation': true },
}
)
})
// expect 438 attestation question to be on the page
- expect(
- screen.getByText(StatutoryRegulatoryAttestationQuestion)
- ).toBeInTheDocument()
+ await waitFor(() => {
+ expect(
+ screen.getByText(StatutoryRegulatoryAttestationQuestion)
+ ).toBeInTheDocument()
+ })
const yesRadio = screen.getByRole('radio', {
name: StatutoryRegulatoryAttestation.YES,
@@ -1073,7 +1070,6 @@ describe('ContractDetails', () => {
})
})
it('errors when continuing without answering 438 attestation question', async () => {
- ldUseClientSpy({ '438-attestation': true })
const draft = mockContractAndRatesDraft({
contractDateStart: new Date('11-12-2023'),
contractDateEnd: new Date('11-12-2024'),
@@ -1090,14 +1086,17 @@ describe('ContractDetails', () => {
/>,
{
apolloProvider: defaultApolloProvider,
+ featureFlags: { '438-attestation': true },
}
)
})
// expect 438 attestation question to be on the page
- expect(
- screen.getByText(StatutoryRegulatoryAttestationQuestion)
- ).toBeInTheDocument()
+ await waitFor(() => {
+ expect(
+ screen.getByText(StatutoryRegulatoryAttestationQuestion)
+ ).toBeInTheDocument()
+ })
const yesRadio = screen.getByRole('radio', {
name: StatutoryRegulatoryAttestation.YES,
@@ -1140,7 +1139,6 @@ describe('ContractDetails', () => {
})
})
it('errors when continuing without description for 438 non-compliance', async () => {
- ldUseClientSpy({ '438-attestation': true })
const draft = mockContractAndRatesDraft({
contractDateStart: new Date('11-12-2023'),
contractDateEnd: new Date('11-12-2024'),
@@ -1157,14 +1155,17 @@ describe('ContractDetails', () => {
/>,
{
apolloProvider: defaultApolloProvider,
+ featureFlags: { '438-attestation': true },
}
)
})
// expect 438 attestation question to be on the page
- expect(
- screen.getByText(StatutoryRegulatoryAttestationQuestion)
- ).toBeInTheDocument()
+ await waitFor(() => {
+ expect(
+ screen.getByText(StatutoryRegulatoryAttestationQuestion)
+ ).toBeInTheDocument()
+ })
const continueButton = screen.getByRole('button', {
name: 'Continue',
diff --git a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx
index 87d6aa9744..49460fdf7c 100644
--- a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx
+++ b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.test.tsx
@@ -17,15 +17,8 @@ import {
mockUnlockedHealthPlanPackage,
mockValidCMSUser,
} from '../../testHelpers/apolloMocks'
-import { ldUseClientSpy } from '../../testHelpers'
describe('SubmissionSideNav', () => {
- beforeEach(() => {
- ldUseClientSpy({ 'cms-questions': true })
- })
- afterEach(() => {
- jest.resetAllMocks()
- })
it('loads sidebar nav with expected links', async () => {
renderWithProviders(
@@ -55,6 +48,7 @@ describe('SubmissionSideNav', () => {
routerProvider: {
route: '/submissions/15',
},
+ featureFlags: { 'cms-questions': true },
}
)
@@ -124,6 +118,7 @@ describe('SubmissionSideNav', () => {
route: '/submissions/15',
},
location: (location) => (testLocation = location),
+ featureFlags: { 'cms-questions': true },
}
)
@@ -230,6 +225,7 @@ describe('SubmissionSideNav', () => {
routerProvider: {
route: '/submissions/15',
},
+ featureFlags: { 'cms-questions': true },
}
)
expect(
@@ -266,6 +262,7 @@ describe('SubmissionSideNav', () => {
routerProvider: {
route: '/submissions/15',
},
+ featureFlags: { 'cms-questions': true },
}
)
@@ -313,6 +310,7 @@ describe('SubmissionSideNav', () => {
routerProvider: {
route: '/submissions/15',
},
+ featureFlags: { 'cms-questions': true },
}
)
@@ -355,6 +353,7 @@ describe('SubmissionSideNav', () => {
routerProvider: {
route: '/submissions/15',
},
+ featureFlags: { 'cms-questions': true },
}
)
@@ -398,6 +397,7 @@ describe('SubmissionSideNav', () => {
route: '/submissions/15',
},
location: (location) => (testLocation = location),
+ featureFlags: { 'cms-questions': true },
}
)
@@ -443,6 +443,7 @@ describe('SubmissionSideNav', () => {
route: '/submissions/15',
},
location: (location) => (testLocation = location),
+ featureFlags: { 'cms-questions': true },
}
)
@@ -487,6 +488,7 @@ describe('SubmissionSideNav', () => {
routerProvider: {
route: '/submissions/15',
},
+ featureFlags: { 'cms-questions': true },
}
)
@@ -521,6 +523,7 @@ describe('SubmissionSideNav', () => {
],
},
routerProvider: { route: '/submissions/404' },
+ featureFlags: { 'cms-questions': true },
}
)
@@ -559,6 +562,7 @@ describe('SubmissionSideNav', () => {
routerProvider: {
route: '/submissions/15/question-and-answers',
},
+ featureFlags: { 'cms-questions': true },
}
)
diff --git a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx
index 2d0cafc331..75ae0e088c 100644
--- a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx
+++ b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx
@@ -1,5 +1,5 @@
import { screen, waitFor } from '@testing-library/react'
-import { renderWithProviders, testS3Client, ldUseClientSpy } from '../../../testHelpers'
+import { renderWithProviders, testS3Client } from '../../../testHelpers'
import {
fetchCurrentUserMock,
fetchRateMockSuccess,
@@ -20,8 +20,6 @@ const wrapInRoutes = (children: React.ReactNode) => {
}
describe('RateSummary', () => {
- afterAll(() => jest.clearAllMocks())
-
describe('Viewing RateSummary as a CMS user', () => {
it('renders without errors', async () => {
renderWithProviders(wrapInRoutes(), {
@@ -38,17 +36,21 @@ describe('RateSummary', () => {
route: '/rates/7a',
},
})
-
+
expect(
- await screen.findByText('Programs this rate certification covers')
+ await screen.findByText(
+ 'Programs this rate certification covers'
+ )
).toBeInTheDocument()
})
it('renders document download warning banner when download fails', async () => {
- const error = jest.spyOn(console, 'error').mockImplementation(() => {
- // mock expected console error to keep test output clear
- })
-
+ const error = jest
+ .spyOn(console, 'error')
+ .mockImplementation(() => {
+ // mock expected console error to keep test output clear
+ })
+
const s3Provider = {
...testS3Client(),
getBulkDlURL: async (
@@ -73,7 +75,7 @@ describe('RateSummary', () => {
},
s3Provider,
})
-
+
await waitFor(() => {
expect(screen.getByTestId('warning-alert')).toBeInTheDocument()
expect(screen.getByTestId('warning-alert')).toHaveClass(
@@ -101,21 +103,17 @@ describe('RateSummary', () => {
route: '/rates/7a',
},
})
-
+
const backLink = await screen.findByRole('link', {
name: /Back to dashboard/,
})
expect(backLink).toBeInTheDocument()
-
+
expect(backLink).toHaveAttribute('href', '/dashboard/rate-reviews')
})
})
describe('Viewing RateSummary as a State user', () => {
- beforeEach(() => {
- ldUseClientSpy({'rate-edit-unlock': true})
- })
-
it('renders without errors', async () => {
renderWithProviders(wrapInRoutes(), {
apolloProvider: {
@@ -128,8 +126,9 @@ describe('RateSummary', () => {
],
},
routerProvider: {
- route: '/rates/1337'
+ route: '/rates/1337',
},
+ featureFlags: { 'rate-edit-unlock': true },
})
await waitFor(() => {
@@ -137,7 +136,9 @@ describe('RateSummary', () => {
})
expect(
- await screen.findByText('Programs this rate certification covers')
+ await screen.findByText(
+ 'Programs this rate certification covers'
+ )
).toBeInTheDocument()
})
@@ -154,13 +155,12 @@ describe('RateSummary', () => {
},
//purposefully attaching invalid id to url here
routerProvider: {
- route: '/rates/133'
+ route: '/rates/133',
},
+ featureFlags: { 'rate-edit-unlock': true },
})
- expect(
- await screen.findByText('System error')
- ).toBeInTheDocument()
+ expect(await screen.findByText('System error')).toBeInTheDocument()
})
it('renders back to dashboard link for state users', async () => {
@@ -177,13 +177,14 @@ describe('RateSummary', () => {
routerProvider: {
route: '/rates/7a',
},
+ featureFlags: { 'rate-edit-unlock': true },
})
-
+
const backLink = await screen.findByRole('link', {
name: /Back to dashboard/,
})
expect(backLink).toBeInTheDocument()
-
+
expect(backLink).toHaveAttribute('href', '/dashboard')
})
})
diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx
index ff487433bc..cdf49b6305 100644
--- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx
+++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.test.tsx
@@ -16,21 +16,12 @@ import {
mockStateSubmission,
mockSubmittedHealthPlanPackage,
} from '../../testHelpers/apolloMocks'
-import {
- ldUseClientSpy,
- renderWithProviders,
-} from '../../testHelpers/jestHelpers'
+import { renderWithProviders } from '../../testHelpers/jestHelpers'
import { SubmissionSummary } from './SubmissionSummary'
import { SubmissionSideNav } from '../SubmissionSideNav'
import { testS3Client } from '../../testHelpers/s3Helpers'
describe('SubmissionSummary', () => {
- beforeEach(() => {
- ldUseClientSpy({ 'cms-questions': false })
- })
- afterEach(() => {
- jest.resetAllMocks()
- })
it('renders without errors', async () => {
renderWithProviders(
diff --git a/services/app-web/src/testHelpers/index.ts b/services/app-web/src/testHelpers/index.ts
index fe1c3524be..0c3e87d7c2 100644
--- a/services/app-web/src/testHelpers/index.ts
+++ b/services/app-web/src/testHelpers/index.ts
@@ -11,7 +11,6 @@ export {
userClickByRole,
userClickByTestId,
userClickSignIn,
- ldUseClientSpy,
TEST_DOC_FILE,
TEST_DOCX_FILE,
TEST_PDF_FILE,
diff --git a/services/app-web/src/testHelpers/jestHelpers.tsx b/services/app-web/src/testHelpers/jestHelpers.tsx
index 278369d6c0..78b7d25ae7 100644
--- a/services/app-web/src/testHelpers/jestHelpers.tsx
+++ b/services/app-web/src/testHelpers/jestHelpers.tsx
@@ -18,7 +18,6 @@ import { PageProvider } from '../contexts/PageContext'
import { S3Provider } from '../contexts/S3Context'
import { testS3Client } from './s3Helpers'
import { S3ClientT } from '../s3'
-import * as LaunchDarkly from 'launchdarkly-react-client-sdk'
import {
FeatureFlagLDConstant,
FlagValue,
@@ -26,6 +25,35 @@ import {
featureFlagKeys,
featureFlags,
} from '../common-code/featureFlags'
+import {
+ LDProvider,
+ ProviderConfig,
+ LDClient,
+} from 'launchdarkly-react-client-sdk'
+
+function ldClientMock(featureFlags: FeatureFlagSettings): LDClient {
+ return {
+ track: jest.fn(),
+ identify: jest.fn(),
+ close: jest.fn(),
+ flush: jest.fn(),
+ getContext: jest.fn(),
+ off: jest.fn(),
+ on: jest.fn(),
+ setStreaming: jest.fn(),
+ variationDetail: jest.fn(),
+ waitForInitialization: jest.fn(),
+ waitUntilGoalsReady: jest.fn(),
+ waitUntilReady: jest.fn(),
+ variation: jest.fn(
+ (
+ flag: FeatureFlagLDConstant,
+ defaultValue: FlagValue | undefined
+ ) => featureFlags[flag] ?? defaultValue
+ ),
+ allFlags: jest.fn(() => featureFlags),
+ }
+}
/* Render */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@@ -37,6 +65,7 @@ const renderWithProviders = (
authProvider?: Partial // used to pass user authentication state via AuthContext
s3Provider?: S3ClientT // used to pass AWS S3 related state via S3Context
location?: (location: Location) => Location // used to pass a location url for react-router
+ featureFlags?: FeatureFlagSettings
}
) => {
const {
@@ -45,23 +74,44 @@ const renderWithProviders = (
authProvider = {},
s3Provider = undefined,
location = undefined,
+ featureFlags = undefined,
} = options || {}
const { route } = routerProvider
const s3Client: S3ClientT = s3Provider ?? testS3Client()
const user = userEvent.setup()
+ const flags: FeatureFlagSettings = {
+ ...getDefaultFeatureFlags(),
+ ...featureFlags,
+ }
+
+ const ldProviderConfig: ProviderConfig = {
+ clientSideID: 'test-url',
+ options: {
+ bootstrap: flags,
+ baseUrl: 'test-url',
+ streamUrl: 'test-url',
+ eventsUrl: 'test-url',
+ },
+ ldClient: ldClientMock(flags),
+ }
+
const renderResult = render(
-
-
-
-
- {location && }
- {ui}
-
-
-
-
+
+
+
+
+
+ {location && (
+
+ )}
+ {ui}
+
+
+
+
+
)
return {
user,
@@ -86,47 +136,6 @@ const getDefaultFeatureFlags = (): FeatureFlagSettings =>
return Object.assign(a, { [flag]: defaultValue })
}, {} as FeatureFlagSettings)
-//WARNING: This required tests using this function to clear mocks afterwards.
-const ldUseClientSpy = (featureFlags: FeatureFlagSettings) => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- jest.spyOn(LaunchDarkly, 'useLDClient').mockImplementation((): any => {
- return {
- // Checks to see if flag passed into useLDClient exists in the featureFlag passed in ldUseClientSpy
- // If flag passed in useLDClient does not exist, then use defaultValue that was also passed into useLDClient.
- // If flag does exist the featureFlag value passed into ldUseClientSpy then use the value in featureFlag.
- //
- // This is done because testing components may contain more than one instance of useLDClient for a different
- // flag. We do not want to apply the value passed in featureFlags to each useLDClient especially if the flag
- // passed in useLDClient does not exist in featureFlags passed into ldUseClientSpy.
- getUser: jest.fn(),
- identify: jest.fn(),
- alias: jest.fn(),
- variation: (
- flag: FeatureFlagLDConstant,
- defaultValue: FlagValue | undefined
- ) => {
- if (
- featureFlags[flag] === undefined &&
- defaultValue === undefined
- ) {
- //ldClient.variation doesn't require a default value, throwing error here if a defaultValue was not provided.
- throw new Error(
- 'ldUseClientSpy returned an invalid value of undefined'
- )
- }
- return featureFlags[flag] === undefined
- ? defaultValue
- : featureFlags[flag]
- },
- allFlags: () => {
- const defaultFeatureFlags = getDefaultFeatureFlags()
- Object.assign(defaultFeatureFlags, featureFlags)
- return defaultFeatureFlags
- },
- }
- })
-}
-
const prettyDebug = (label?: string, element?: HTMLElement): void => {
console.info(
`${label ?? 'body'}:
@@ -238,7 +247,6 @@ export {
userClickByRole,
userClickByTestId,
userClickSignIn,
- ldUseClientSpy,
selectYesNoRadio,
TEST_DOC_FILE,
TEST_DOCX_FILE,
diff --git a/yarn.lock b/yarn.lock
index 66cef85390..42a34ba21b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -22863,10 +22863,10 @@ launchdarkly-eventsource@2.0.0:
resolved "https://registry.yarnpkg.com/launchdarkly-eventsource/-/launchdarkly-eventsource-2.0.0.tgz#2832e73fa8bc0c103a7f8c6dbc3b1b53d22f9acc"
integrity sha512-fxZ4IN46juAc3s8/geiutRPbI8cvUBz0Lcsayh3wfd97edYWLIsnaThw2esQ3zc6vgZ1v5IjTbdumNgoT3iRnw==
-launchdarkly-js-client-sdk@^3.1.3:
- version "3.1.3"
- resolved "https://registry.yarnpkg.com/launchdarkly-js-client-sdk/-/launchdarkly-js-client-sdk-3.1.3.tgz#e046439f0e4f0bfd6d38b9eaa4420a6e40ffc0c7"
- integrity sha512-/JR/ri8z3bEj9RFTTKDjd+con4F1MsWUea1MmBDtFj4gDA0l9NDm1KzhMKiIeoBdmB2rSaeFYe4CaYOEp8IryA==
+launchdarkly-js-client-sdk@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.yarnpkg.com/launchdarkly-js-client-sdk/-/launchdarkly-js-client-sdk-3.1.4.tgz#e613cb53412533c07ccf140ae570fc994c59758d"
+ integrity sha512-yq0FeklpVuHMSRz7jfUAfyM7I/659RvGztqJ0Y9G5eN/ZrG1o2W61ZU0Nrv/gqZCtLXjarh/u1otxSFFBjTpHw==
dependencies:
escape-string-regexp "^4.0.0"
launchdarkly-js-sdk-common "5.0.3"
@@ -22880,13 +22880,13 @@ launchdarkly-js-sdk-common@5.0.3:
fast-deep-equal "^2.0.1"
uuid "^8.0.0"
-launchdarkly-react-client-sdk@^3.0.1:
- version "3.0.6"
- resolved "https://registry.yarnpkg.com/launchdarkly-react-client-sdk/-/launchdarkly-react-client-sdk-3.0.6.tgz#5c694a4a013757d2afb5213efd28d9c16af1595e"
- integrity sha512-r7gSshScugjnJB4lJ6mAOMKpV4Pj/wUks3tsRHdfeXgER9jPdxmZOAkm0besMjK0S7lfQdjxJ2KIXrh+Mn1sQA==
+launchdarkly-react-client-sdk@^3.0.10:
+ version "3.0.10"
+ resolved "https://registry.yarnpkg.com/launchdarkly-react-client-sdk/-/launchdarkly-react-client-sdk-3.0.10.tgz#33816d939d9bd18b0723c0fd30b4772f2429f3de"
+ integrity sha512-ssb3KWe9z42+q8X2u32OrlDntGLsv0NP/p4E2Hx4O9RU0OeFm9v6omOlIk9SMsYEQD4QzLSXAp5L3cSN2ssLlA==
dependencies:
hoist-non-react-statics "^3.3.2"
- launchdarkly-js-client-sdk "^3.1.3"
+ launchdarkly-js-client-sdk "^3.1.4"
lodash.camelcase "^4.3.0"
lazy-ass@^1.6.0:
@@ -28508,7 +28508,6 @@ serverless-plugin-scripts@^1.0.2:
serverless-s3-bucket-helper@CMSgov/serverless-s3-bucket-helper:
version "1.0.0"
- uid "3e519d15676de237ec8ede3ff9ae26abf3f3ef0a"
resolved "https://codeload.github.com/CMSgov/serverless-s3-bucket-helper/tar.gz/3e519d15676de237ec8ede3ff9ae26abf3f3ef0a"
serverless-s3-local@^0.7.1:
From 7e7c2e0a353acf41846a113af5d9a47c70dae2e2 Mon Sep 17 00:00:00 2001
From: MacRae Linton <55759+macrael@users.noreply.github.com>
Date: Sun, 28 Jan 2024 21:30:09 -0800
Subject: [PATCH 02/24] Setup Secrets for JWT (#2189)
* initial pass at threading secrets for jwt
* make the variable handling work again local and deployed
---
.envrc | 3 +-
services/app-api/serverless.yml | 4 +
services/app-api/src/handlers/apollo_gql.ts | 12 ++-
.../handlers/third_party_API_authorizer.ts | 20 +++-
services/app-api/src/jwt/jwt.test.ts | 16 +--
services/app-api/src/jwt/jwt.ts | 6 +-
.../src/resolvers/APIKey/createAPIKey.test.ts | 2 +-
.../app-api/src/testHelpers/gqlHelpers.ts | 2 +-
.../ActionButton/ActionButton.module.scss | 10 +-
.../src/components/Banner/Banner.module.scss | 6 +-
.../Breadcrumbs/Breadcrumbs.module.scss | 2 +-
.../src/components/Colors/Colors.module.scss | 98 +++++++++----------
.../DataDetail/DataDetail.module.scss | 1 -
.../InlineDocumentWarning.module.scss | 6 +-
.../DownloadButton/DownloadButton.module.scss | 8 +-
.../DynamicStepIndicator.module.scss | 3 +-
.../ErrorAlert/ErrorAlert.module.scss | 10 +-
.../ExpandableText/ExpandableText.module.scss | 2 +-
.../FilterDateRange.module.scss | 28 +++---
.../ErrorSummary/ErrorSummary.module.scss | 5 +-
.../FieldTextarea/FieldTextarea.module.scss | 2 +-
.../Form/FieldYesNo/FieldYesNo.module.scss | 6 +-
.../src/components/Header/Header.module.scss | 6 +-
.../src/components/Logo/Logo.module.scss | 4 +-
.../src/components/Modal/Modal.module.scss | 8 +-
.../Modal/UnlockSubmitModal.module.scss | 3 +-
.../SectionCard/SectionCard.module.scss | 10 +-
.../src/components/Select/Select.module.scss | 37 +++----
.../SubmissionCard/SubmissionCard.module.scss | 3 +-
.../SubmissionSummarySection.module.scss | 18 ++--
.../UploadedDocumentsTable.module.scss | 8 +-
.../src/components/Tabs/Tabs.module.scss | 5 +-
.../src/localAuth/LocalLogin.module.scss | 2 +-
.../app-web/src/pages/App/AppBody.module.scss | 2 +-
.../src/pages/Errors/Errors.module.scss | 2 +-
.../GraphQLExplorer.module.scss | 14 +--
.../app-web/src/pages/Help/Help.module.scss | 3 +-
.../src/pages/Landing/Landing.module.scss | 2 -
.../src/pages/MccrsId/MccrsId.module.scss | 12 +--
.../QATable/QATable.module.scss | 29 +++---
.../QuestionResponse.module.scss | 47 +++++----
.../src/pages/Settings/Settings.module.scss | 12 +--
.../StateSubmissionForm.module.scss | 18 ++--
.../SubmissionRevisionSummary.module.scss | 2 +-
.../SubmissionSideNav.module.scss | 28 +++---
.../RateSummary/RateSummary.tsx | 5 +-
.../SubmissionSummary.module.scss | 7 +-
services/app-web/src/styles/custom.scss | 11 ++-
services/app-web/src/styles/mcrColors.scss | 24 ++---
services/app-web/src/styles/overrides.scss | 26 ++---
services/app-web/src/styles/theme/_color.scss | 2 -
.../app-web/src/styles/uswdsSettings.scss | 1 -
services/infra-api/serverless.yml | 41 +++-----
53 files changed, 321 insertions(+), 323 deletions(-)
diff --git a/.envrc b/.envrc
index e60feb2742..3fa8c01ecb 100644
--- a/.envrc
+++ b/.envrc
@@ -12,7 +12,7 @@ export CT_URL='https://cloudtamer.cms.gov/'
export CT_AWS_ROLE='ct-ado-managedcare-developer-admin'
export CT_IDMS='2'
-# values formerly in .env (required)
+# required values
export SASS_PATH='src:../../node_modules'
export REACT_APP_AUTH_MODE='LOCAL'
export REACT_APP_STAGE_NAME='local'
@@ -26,6 +26,7 @@ export DATABASE_URL='postgresql://postgres:shhhsecret@localhost:5432/postgres?sc
export EMAILER_MODE='LOCAL'
export LD_SDK_KEY='this-value-must-be-set-in-local'
export PARAMETER_STORE_MODE='LOCAL'
+export JWT_SECRET='3fd2e448ed2cec1fa46520f1b64bcb243c784f68db41ea67ef9abc45c12951d3e770162829103c439f01d2b860d06ed0da1a08895117b1ef338f1e4ed176448a' # pragma: allowlist secret
export REACT_APP_OTEL_COLLECTOR_URL='http://localhost:4318/v1/traces'
export REACT_APP_LD_CLIENT_ID='this-value-can-be-set-in-local-if-desired'
diff --git a/services/app-api/serverless.yml b/services/app-api/serverless.yml
index abb3d73f1b..76e4d5e5d0 100644
--- a/services/app-api/serverless.yml
+++ b/services/app-api/serverless.yml
@@ -34,6 +34,9 @@ custom:
reactAppOtelCollectorUrl: ${env:REACT_APP_OTEL_COLLECTOR_URL, ssm:/configuration/react_app_otel_collector_url}
dbURL: ${env:DATABASE_URL}
ldSDKKey: ${env:LD_SDK_KEY, ssm:/configuration/ld_sdk_key_feds}
+ # because the secret is in JSON in secret manager, we have to pass it into jwtSecret when not running locally
+ jwtSecretJSON: ${env:CF_CONFIG_IGNORED_LOCALLY, ssm:/aws/reference/secretsmanager/api_jwt_secret_wmltestapijwtaccess}
+ jwtSecret: ${env:JWT_SECRET, self:custom.jwtSecretJSON.jwtsigningkey}
webpack:
webpackConfig: 'webpack.config.js'
packager: 'yarn'
@@ -154,6 +157,7 @@ provider:
AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-handler
OPENTELEMETRY_COLLECTOR_CONFIG_FILE: /var/task/collector.yml
LD_SDK_KEY: ${self:custom.ldSDKKey}
+ JWT_SECRET: ${self:custom.jwtSecret}
layers:
prismaClientMigration:
diff --git a/services/app-api/src/handlers/apollo_gql.ts b/services/app-api/src/handlers/apollo_gql.ts
index 1634285734..1377e56099 100644
--- a/services/app-api/src/handlers/apollo_gql.ts
+++ b/services/app-api/src/handlers/apollo_gql.ts
@@ -177,6 +177,7 @@ async function initializeGQLHandler(): Promise {
const otelCollectorUrl = process.env.REACT_APP_OTEL_COLLECTOR_URL
const parameterStoreMode = process.env.PARAMETER_STORE_MODE
const ldSDKKey = process.env.LD_SDK_KEY
+ const jwtSecret = process.env.JWT_SECRET
// START Assert configuration is valid
if (emailerMode !== 'LOCAL' && emailerMode !== 'SES')
@@ -212,6 +213,13 @@ async function initializeGQLHandler(): Promise {
'Configuration Error: LD_SDK_KEY is required to run app-api.'
)
}
+
+ if (jwtSecret === undefined || jwtSecret === '') {
+ throw new Error(
+ 'Configuration Error: JWT_SECRET is required to run app-api.'
+ )
+ }
+
// END
const pgResult = await configurePostgres(dbURL, secretsManagerSecret)
@@ -310,8 +318,8 @@ async function initializeGQLHandler(): Promise {
// Hard coding this for now, next job is to run this config to this app.
const jwtLib = newJWTLib({
- issuer: 'fakeIssuer',
- signingKey: 'notrandom',
+ issuer: `mcreview-${stageName}`,
+ signingKey: Buffer.from(jwtSecret, 'hex'),
expirationDurationS: 90 * 24 * 60 * 60, // 90 days
})
diff --git a/services/app-api/src/handlers/third_party_API_authorizer.ts b/services/app-api/src/handlers/third_party_API_authorizer.ts
index a3741ba476..61b0e58083 100644
--- a/services/app-api/src/handlers/third_party_API_authorizer.ts
+++ b/services/app-api/src/handlers/third_party_API_authorizer.ts
@@ -6,10 +6,22 @@ import type {
} from 'aws-lambda'
import { newJWTLib } from '../jwt'
-// Hard coding this for now, next job is to run this config to this app.
+const stageName = process.env.stage
+const jwtSecret = process.env.JWT_SECRET
+
+if (stageName === undefined) {
+ throw new Error('Configuration Error: stage is required')
+}
+
+if (jwtSecret === undefined || jwtSecret === '') {
+ throw new Error(
+ 'Configuration Error: JWT_SECRET is required to run app-api.'
+ )
+}
+
const jwtLib = newJWTLib({
- issuer: 'fakeIssuer',
- signingKey: 'notrandom',
+ issuer: `mcreview-${stageName}`,
+ signingKey: Buffer.from(jwtSecret, 'hex'),
expirationDurationS: 90 * 24 * 60 * 60, // 90 days
})
@@ -19,7 +31,7 @@ export const main: APIGatewayTokenAuthorizerHandler = async (
const authToken = event.authorizationToken.replace('Bearer ', '')
try {
// authentication step for validating JWT token
- const userId = await jwtLib.userIDFromToken(authToken)
+ const userId = jwtLib.userIDFromToken(authToken)
if (userId instanceof Error) {
const msg = 'Invalid auth token'
diff --git a/services/app-api/src/jwt/jwt.test.ts b/services/app-api/src/jwt/jwt.test.ts
index bdec778eee..d5eb3505d3 100644
--- a/services/app-api/src/jwt/jwt.test.ts
+++ b/services/app-api/src/jwt/jwt.test.ts
@@ -4,7 +4,7 @@ describe('jwtLib', () => {
it('works symmetricly', () => {
const jwt = newJWTLib({
issuer: 'mctest',
- signingKey: 'foo', //pragma: allowlist secret
+ signingKey: Buffer.from('123af', 'hex'),
expirationDurationS: 1000,
})
@@ -27,13 +27,13 @@ describe('jwtLib', () => {
it('errors with wrong issuer', () => {
const jwtWriter = newJWTLib({
issuer: 'wrong',
- signingKey: 'foo',
+ signingKey: Buffer.from('123af', 'hex'),
expirationDurationS: 1000,
})
const jwtReader = newJWTLib({
issuer: 'mctest',
- signingKey: 'foo',
+ signingKey: Buffer.from('123af', 'hex'),
expirationDurationS: 1000,
})
@@ -49,13 +49,13 @@ describe('jwtLib', () => {
it('errors with bad expiration', () => {
const jwtWriter = newJWTLib({
issuer: 'mctest',
- signingKey: 'foo',
+ signingKey: Buffer.from('123af', 'hex'),
expirationDurationS: 0,
})
const jwtReader = newJWTLib({
issuer: 'mctest',
- signingKey: 'foo',
+ signingKey: Buffer.from('123af', 'hex'),
expirationDurationS: 1000,
})
@@ -71,13 +71,13 @@ describe('jwtLib', () => {
it('errors with bad secret', () => {
const jwtWriter = newJWTLib({
issuer: 'mctest',
- signingKey: 'wrong',
+ signingKey: Buffer.from('deadbeef', 'hex'),
expirationDurationS: 1000,
})
const jwtReader = newJWTLib({
issuer: 'mctest',
- signingKey: 'foo',
+ signingKey: Buffer.from('123af', 'hex'),
expirationDurationS: 1000,
})
@@ -93,7 +93,7 @@ describe('jwtLib', () => {
it('errors with bogus JWT', () => {
const jwtReader = newJWTLib({
issuer: 'mctest',
- signingKey: 'foo',
+ signingKey: Buffer.from('123af', 'hex'),
expirationDurationS: 1000,
})
diff --git a/services/app-api/src/jwt/jwt.ts b/services/app-api/src/jwt/jwt.ts
index a1da0c27fd..c817e801d0 100644
--- a/services/app-api/src/jwt/jwt.ts
+++ b/services/app-api/src/jwt/jwt.ts
@@ -4,7 +4,7 @@ import { sign, verify } from 'jsonwebtoken'
interface JWTConfig {
issuer: string
- signingKey: string
+ signingKey: Buffer
expirationDurationS: number
}
@@ -13,6 +13,7 @@ function createValidJWT(config: JWTConfig, userID: string): APIKeyType {
subject: userID,
issuer: config.issuer,
expiresIn: config.expirationDurationS,
+ algorithm: 'HS256', // pin the default algo
})
return {
@@ -25,6 +26,7 @@ function userIDFromToken(config: JWTConfig, token: string): string | Error {
try {
const decoded = verify(token, config.signingKey, {
issuer: config.issuer,
+ algorithms: ['HS256'], // pin the default algo
})
if (!decoded.sub || typeof decoded === 'string') {
@@ -45,6 +47,8 @@ interface JWTLib {
function newJWTLib(config: JWTConfig): JWTLib {
return {
+ // this is an experiment, using `curry` here, It seems clean but I'm not sure
+ // exactly what it's getting us yet -wml
createValidJWT: curry(createValidJWT)(config),
userIDFromToken: curry(userIDFromToken)(config),
}
diff --git a/services/app-api/src/resolvers/APIKey/createAPIKey.test.ts b/services/app-api/src/resolvers/APIKey/createAPIKey.test.ts
index b6397de517..cf54dc355d 100644
--- a/services/app-api/src/resolvers/APIKey/createAPIKey.test.ts
+++ b/services/app-api/src/resolvers/APIKey/createAPIKey.test.ts
@@ -7,7 +7,7 @@ describe('createAPIKey', () => {
it('creates a new API key', async () => {
const jwt = newJWTLib({
issuer: 'mctestiss',
- signingKey: 'foo',
+ signingKey: Buffer.from('123af', 'hex'),
expirationDurationS: 1000,
})
diff --git a/services/app-api/src/testHelpers/gqlHelpers.ts b/services/app-api/src/testHelpers/gqlHelpers.ts
index 0f07c15dfa..e2f7cc706f 100644
--- a/services/app-api/src/testHelpers/gqlHelpers.ts
+++ b/services/app-api/src/testHelpers/gqlHelpers.ts
@@ -91,7 +91,7 @@ const constructTestPostgresServer = async (opts?: {
opts?.jwt ||
newJWTLib({
issuer: 'mcreviewtest',
- signingKey: 'foo',
+ signingKey: Buffer.from('123af', 'hex'),
expirationDurationS: 1000,
})
diff --git a/services/app-web/src/components/ActionButton/ActionButton.module.scss b/services/app-web/src/components/ActionButton/ActionButton.module.scss
index 9bd3da73f4..edaf911800 100644
--- a/services/app-web/src/components/ActionButton/ActionButton.module.scss
+++ b/services/app-web/src/components/ActionButton/ActionButton.module.scss
@@ -3,16 +3,16 @@
// preferred way to show a button that is disabled. avoid cursor: none
.disabledCursor {
- cursor: not-allowed;
+ cursor: not-allowed;
}
.buttonTextWithIcon {
- vertical-align: middle;
- margin-left: .5rem
+ vertical-align: middle;
+ margin-left: 0.5rem;
}
.buttonTextWithoutIcon {
- vertical-align: middle;
+ vertical-align: middle;
}
.successButton {
@@ -21,6 +21,6 @@
background-color: custom.$mcr-success-hover !important;
}
&:active {
- background-color: custom.$mcr-success-hover !important;
+ background-color: custom.$mcr-success-hover !important;
}
}
diff --git a/services/app-web/src/components/Banner/Banner.module.scss b/services/app-web/src/components/Banner/Banner.module.scss
index 2392b4f24e..fa64f2cedc 100644
--- a/services/app-web/src/components/Banner/Banner.module.scss
+++ b/services/app-web/src/components/Banner/Banner.module.scss
@@ -2,7 +2,7 @@
@use '../../styles/uswdsImports.scss' as uswds;
.bannerBodyText {
- p {
- margin: 0
- }
+ p {
+ margin: 0;
+ }
}
diff --git a/services/app-web/src/components/Breadcrumbs/Breadcrumbs.module.scss b/services/app-web/src/components/Breadcrumbs/Breadcrumbs.module.scss
index 901034450e..41fe506a52 100644
--- a/services/app-web/src/components/Breadcrumbs/Breadcrumbs.module.scss
+++ b/services/app-web/src/components/Breadcrumbs/Breadcrumbs.module.scss
@@ -9,7 +9,7 @@
[class^='usa-breadcrumb'] {
li:last-child a {
text-decoration: none;
- color: custom.$mcr-foundation-ink
+ color: custom.$mcr-foundation-ink;
}
}
}
diff --git a/services/app-web/src/components/Colors/Colors.module.scss b/services/app-web/src/components/Colors/Colors.module.scss
index ca720e3425..33d373327a 100644
--- a/services/app-web/src/components/Colors/Colors.module.scss
+++ b/services/app-web/src/components/Colors/Colors.module.scss
@@ -2,55 +2,53 @@
@use '../../styles/custom.scss' as custom;
:export {
- mcr: {
- primary: {
- lighter: custom.$mcr-primary-lighter;
- light: custom.$mcr-primary-light;
- base: custom.$mcr-primary-base;
- dark: custom.$mcr-primary-dark;
- darkest: custom.$mcr-primary-darkest;
- }
- cmsblue: {
- lightest: custom.$mcr-cmsblue-lightest;
- base: custom.$mcr-cmsblue-base;
- dark: custom.$mcr-cmsblue-dark;
- darkest: custom.$mcr-cmsblue-darkest;
- }
- cyan: {
- light: custom.$mcr-cyan-light;
- base: custom.$mcr-cyan-base;
- dark: custom.$mcr-cyan-dark;
- }
- gold: {
- base: custom.$mcr-gold-base;
- dark: custom.$mcr-gold-dark;
- darker: custom.$mcr-gold-darker;
- }
- gray: {
- dark: custom.$mcr-gray-dark;
- base : custom.$mcr-gray-base;
- lighter: custom.$mcr-gray-lighter;
- lightest: custom.$mcr-gray-lightest;
- }
- foundation: {
- white: custom.$mcr-foundation-white;
- ink : custom.$mcr-foundation-ink;
- hint: custom.$mcr-foundation-hint;
- link: custom.$mcr-foundation-link;
- focus: custom.$mcr-foundation-focus;
- visited: custom.$mcr-foundation-visited;
-
- }
- success: {
- base: custom.$mcr-success-base;
- hover: custom.$mcr-success-hover;
- dark: custom.$mcr-success-dark;
- }
- error: {
- light: custom.$mcr-error-light;
- base: custom.$mcr-error-base;
- dark: custom.$mcr-error-dark;
+ mcr: {
+ primary: {
+ lighter: custom.$mcr-primary-lighter;
+ light: custom.$mcr-primary-light;
+ base: custom.$mcr-primary-base;
+ dark: custom.$mcr-primary-dark;
+ darkest: custom.$mcr-primary-darkest;
+ }
+ cmsblue: {
+ lightest: custom.$mcr-cmsblue-lightest;
+ base: custom.$mcr-cmsblue-base;
+ dark: custom.$mcr-cmsblue-dark;
+ darkest: custom.$mcr-cmsblue-darkest;
+ }
+ cyan: {
+ light: custom.$mcr-cyan-light;
+ base: custom.$mcr-cyan-base;
+ dark: custom.$mcr-cyan-dark;
+ }
+ gold: {
+ base: custom.$mcr-gold-base;
+ dark: custom.$mcr-gold-dark;
+ darker: custom.$mcr-gold-darker;
+ }
+ gray: {
+ dark: custom.$mcr-gray-dark;
+ base: custom.$mcr-gray-base;
+ lighter: custom.$mcr-gray-lighter;
+ lightest: custom.$mcr-gray-lightest;
+ }
+ foundation: {
+ white: custom.$mcr-foundation-white;
+ ink: custom.$mcr-foundation-ink;
+ hint: custom.$mcr-foundation-hint;
+ link: custom.$mcr-foundation-link;
+ focus: custom.$mcr-foundation-focus;
+ visited: custom.$mcr-foundation-visited;
+ }
+ success: {
+ base: custom.$mcr-success-base;
+ hover: custom.$mcr-success-hover;
+ dark: custom.$mcr-success-dark;
+ }
+ error: {
+ light: custom.$mcr-error-light;
+ base: custom.$mcr-error-base;
+ dark: custom.$mcr-error-dark;
+ }
}
- }
-
}
diff --git a/services/app-web/src/components/DataDetail/DataDetail.module.scss b/services/app-web/src/components/DataDetail/DataDetail.module.scss
index c9d63838c2..7f56210cf9 100644
--- a/services/app-web/src/components/DataDetail/DataDetail.module.scss
+++ b/services/app-web/src/components/DataDetail/DataDetail.module.scss
@@ -17,7 +17,6 @@
margin: 0;
line-height: 1.5;
}
-
}
.missingInfo {
diff --git a/services/app-web/src/components/DocumentWarning/InlineDocumentWarning/InlineDocumentWarning.module.scss b/services/app-web/src/components/DocumentWarning/InlineDocumentWarning/InlineDocumentWarning.module.scss
index 0ccbc70d43..48ad699876 100644
--- a/services/app-web/src/components/DocumentWarning/InlineDocumentWarning/InlineDocumentWarning.module.scss
+++ b/services/app-web/src/components/DocumentWarning/InlineDocumentWarning/InlineDocumentWarning.module.scss
@@ -2,7 +2,7 @@
@use '../../../styles/uswdsImports.scss' as uswds;
.missingInfo {
- color: custom.$mcr-gold-darker;
- font-weight: 700;
- display: flex;
+ color: custom.$mcr-gold-darker;
+ font-weight: 700;
+ display: flex;
}
diff --git a/services/app-web/src/components/DownloadButton/DownloadButton.module.scss b/services/app-web/src/components/DownloadButton/DownloadButton.module.scss
index fe85336ec7..036ebe080f 100644
--- a/services/app-web/src/components/DownloadButton/DownloadButton.module.scss
+++ b/services/app-web/src/components/DownloadButton/DownloadButton.module.scss
@@ -2,14 +2,14 @@
@use '../../styles/uswdsImports.scss' as uswds;
.disabledCursor {
- cursor: not-allowed;
+ cursor: not-allowed;
}
.buttonTextWithIcon {
- vertical-align: middle;
- margin-left: .5rem
+ vertical-align: middle;
+ margin-left: 0.5rem;
}
.buttonTextWithoutIcon {
- vertical-align: middle;
+ vertical-align: middle;
}
diff --git a/services/app-web/src/components/DynamicStepIndicator/DynamicStepIndicator.module.scss b/services/app-web/src/components/DynamicStepIndicator/DynamicStepIndicator.module.scss
index 1ad0681481..c6ea7fef30 100644
--- a/services/app-web/src/components/DynamicStepIndicator/DynamicStepIndicator.module.scss
+++ b/services/app-web/src/components/DynamicStepIndicator/DynamicStepIndicator.module.scss
@@ -8,8 +8,7 @@
justify-content: center;
}
-
[class^='usa-step-indicator__header'] {
display: inline;
}
-}
\ No newline at end of file
+}
diff --git a/services/app-web/src/components/ErrorAlert/ErrorAlert.module.scss b/services/app-web/src/components/ErrorAlert/ErrorAlert.module.scss
index 28eea9aeae..5fabe03c7f 100644
--- a/services/app-web/src/components/ErrorAlert/ErrorAlert.module.scss
+++ b/services/app-web/src/components/ErrorAlert/ErrorAlert.module.scss
@@ -1,12 +1,12 @@
@use '../../styles/custom.scss' as custom;
@use '../../styles/uswdsImports.scss' as uswds;
-.messageBodyText{
- p {
- margin: 0
- }
+.messageBodyText {
+ p {
+ margin: 0;
+ }
}
.nowrap {
- white-space: nowrap;
+ white-space: nowrap;
}
diff --git a/services/app-web/src/components/ExpandableText/ExpandableText.module.scss b/services/app-web/src/components/ExpandableText/ExpandableText.module.scss
index 359d92a257..ea08f435fb 100644
--- a/services/app-web/src/components/ExpandableText/ExpandableText.module.scss
+++ b/services/app-web/src/components/ExpandableText/ExpandableText.module.scss
@@ -21,5 +21,5 @@
background-color: transparent;
border: none;
cursor: pointer;
- margin-top: 0.50rem;
+ margin-top: 0.5rem;
}
diff --git a/services/app-web/src/components/FilterAccordion/FilterDateRange/FilterDateRange.module.scss b/services/app-web/src/components/FilterAccordion/FilterDateRange/FilterDateRange.module.scss
index 286388fb5e..ca4ad1a0f5 100644
--- a/services/app-web/src/components/FilterAccordion/FilterDateRange/FilterDateRange.module.scss
+++ b/services/app-web/src/components/FilterAccordion/FilterDateRange/FilterDateRange.module.scss
@@ -2,20 +2,20 @@
@use '../../../styles/uswdsImports.scss' as uswds;
.dateRangePicker {
- [class='usa-label'] {
- font-weight: normal;
- }
- [class='usa-legend'] {
- font-weight: normal;
- }
+ [class='usa-label'] {
+ font-weight: normal;
+ }
+ [class='usa-legend'] {
+ font-weight: normal;
+ }
- [class='usa-form-group'] {
- margin-top: 0rem;
- width: 100%;
- }
+ [class='usa-form-group'] {
+ margin-top: 0rem;
+ width: 100%;
+ }
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- column-gap: 32px;
- align-items: end;
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ column-gap: 32px;
+ align-items: end;
}
diff --git a/services/app-web/src/components/Form/ErrorSummary/ErrorSummary.module.scss b/services/app-web/src/components/Form/ErrorSummary/ErrorSummary.module.scss
index 561a099fcc..a4b55a7f50 100644
--- a/services/app-web/src/components/Form/ErrorSummary/ErrorSummary.module.scss
+++ b/services/app-web/src/components/Form/ErrorSummary/ErrorSummary.module.scss
@@ -8,7 +8,8 @@
cursor: pointer;
font-weight: normal;
text-align: left;
- &, &:visited {
+ &,
+ &:visited {
color: custom.$mcr-foundation-ink;
}
}
@@ -22,4 +23,4 @@
padding-left: 1rem;
}
margin-bottom: 1rem;
-}
\ No newline at end of file
+}
diff --git a/services/app-web/src/components/Form/FieldTextarea/FieldTextarea.module.scss b/services/app-web/src/components/Form/FieldTextarea/FieldTextarea.module.scss
index d031dcbcb2..ad94ef721c 100644
--- a/services/app-web/src/components/Form/FieldTextarea/FieldTextarea.module.scss
+++ b/services/app-web/src/components/Form/FieldTextarea/FieldTextarea.module.scss
@@ -4,4 +4,4 @@
.requiredOptionalText {
display: block;
color: custom.$mcr-foundation-hint;
-}
\ No newline at end of file
+}
diff --git a/services/app-web/src/components/Form/FieldYesNo/FieldYesNo.module.scss b/services/app-web/src/components/Form/FieldYesNo/FieldYesNo.module.scss
index 1ae5145129..eab31d1af6 100644
--- a/services/app-web/src/components/Form/FieldYesNo/FieldYesNo.module.scss
+++ b/services/app-web/src/components/Form/FieldYesNo/FieldYesNo.module.scss
@@ -6,11 +6,11 @@
}
.optionsContainer label {
- margin-top: .8em;
+ margin-top: 0.8em;
}
.yesnofieldsecondary {
- margin-top: .8em;
+ margin-top: 0.8em;
margin-bottom: 1.5em;
}
@@ -25,4 +25,4 @@
.requiredOptionalText {
display: block;
color: custom.$mcr-foundation-hint;
-}
\ No newline at end of file
+}
diff --git a/services/app-web/src/components/Header/Header.module.scss b/services/app-web/src/components/Header/Header.module.scss
index 97ca6dc9bb..a44175c323 100644
--- a/services/app-web/src/components/Header/Header.module.scss
+++ b/services/app-web/src/components/Header/Header.module.scss
@@ -6,7 +6,8 @@
padding: 0 uswds.units(1);
color: custom.$mcr-foundation-white;
- button, a {
+ button,
+ a {
color: custom.$mcr-foundation-white;
&:hover,
@@ -21,7 +22,6 @@
padding: 0 uswds.units(1);
}
-
.landingPageHeading {
background-color: custom.$mcr-cmsblue-dark;
color: custom.$mcr-foundation-white;
@@ -59,7 +59,7 @@
}
.dashboardHeading {
- background-color: custom.$mcr-cmsblue-dark;;
+ background-color: custom.$mcr-cmsblue-dark;
color: custom.$mcr-foundation-white;
& h1 {
display: flex;
diff --git a/services/app-web/src/components/Logo/Logo.module.scss b/services/app-web/src/components/Logo/Logo.module.scss
index 64498b737a..7f9864a696 100644
--- a/services/app-web/src/components/Logo/Logo.module.scss
+++ b/services/app-web/src/components/Logo/Logo.module.scss
@@ -1,4 +1,4 @@
-.override{
+.override {
margin-bottom: 1rem;
margin-top: 1rem;
-}
\ No newline at end of file
+}
diff --git a/services/app-web/src/components/Modal/Modal.module.scss b/services/app-web/src/components/Modal/Modal.module.scss
index 01eb7c0a6e..622e5b871b 100644
--- a/services/app-web/src/components/Modal/Modal.module.scss
+++ b/services/app-web/src/components/Modal/Modal.module.scss
@@ -2,10 +2,10 @@
@use '../../styles/uswdsImports.scss' as uswds;
.modal {
- max-width: 50rem;
- div {
+ max-width: 50rem;
div {
- margin: 0;
+ div {
+ margin: 0;
+ }
}
- }
}
diff --git a/services/app-web/src/components/Modal/UnlockSubmitModal.module.scss b/services/app-web/src/components/Modal/UnlockSubmitModal.module.scss
index dfbb775b5b..6cb877323f 100644
--- a/services/app-web/src/components/Modal/UnlockSubmitModal.module.scss
+++ b/services/app-web/src/components/Modal/UnlockSubmitModal.module.scss
@@ -5,11 +5,10 @@
max-width: custom.$mcr-container-standard-width-fixed;
}
-
.submitButton {
background: custom.$mcr-success-base;
&:hover {
- background-color: custom.$mcr-success-hover !important;
+ background-color: custom.$mcr-success-hover !important;
}
}
diff --git a/services/app-web/src/components/SectionCard/SectionCard.module.scss b/services/app-web/src/components/SectionCard/SectionCard.module.scss
index 7970b61b72..48712cea78 100644
--- a/services/app-web/src/components/SectionCard/SectionCard.module.scss
+++ b/services/app-web/src/components/SectionCard/SectionCard.module.scss
@@ -1,10 +1,10 @@
@use '../../styles/custom.scss' as custom;
.section {
- @include custom.sectionCard;
-
- >h3, fieldset>h3{
- margin-top: 0; // adjust vertical space between section edge and the first heading
- }
+ @include custom.sectionCard;
+ > h3,
+ fieldset > h3 {
+ margin-top: 0; // adjust vertical space between section edge and the first heading
+ }
}
diff --git a/services/app-web/src/components/Select/Select.module.scss b/services/app-web/src/components/Select/Select.module.scss
index dc213cdac8..86090d3646 100644
--- a/services/app-web/src/components/Select/Select.module.scss
+++ b/services/app-web/src/components/Select/Select.module.scss
@@ -4,23 +4,26 @@
// react-select draws the chips in two parts, so we round the outer and square in the inner corners
.multiSelect {
- padding-top: 12px;
- [class*='select__multi-value'] {
- border-radius: 99rem 99rem 99rem 99rem;
- }
- [class*='select__multi-value__label'] {
- background:custom.$mcr-primary-lighter;
- color: custom.$mcr-foundation-ink;
- border-radius: 99rem 0rem 0rem 99rem;
- }
- [class*='select__multi-value__remove'] {
- background:custom.$mcr-primary-lighter;
- color: custom.$mcr-foundation-ink;
- border-radius: 0rem 99rem 99rem 0rem;
+ padding-top: 12px;
+ [class*='select__multi-value'] {
+ border-radius: 99rem 99rem 99rem 99rem;
+ }
+ [class*='select__multi-value__label'] {
+ background: custom.$mcr-primary-lighter;
+ color: custom.$mcr-foundation-ink;
+ border-radius: 99rem 0rem 0rem 99rem;
+ }
+ [class*='select__multi-value__remove'] {
+ background: custom.$mcr-primary-lighter;
+ color: custom.$mcr-foundation-ink;
+ border-radius: 0rem 99rem 99rem 0rem;
- &:hover {
- background: color.adjust(custom.$mcr-primary-lighter, $lightness: -5);
- color: custom.$mcr-foundation-ink;
+ &:hover {
+ background: color.adjust(
+ custom.$mcr-primary-lighter,
+ $lightness: -5
+ );
+ color: custom.$mcr-foundation-ink;
+ }
}
- }
}
diff --git a/services/app-web/src/components/SubmissionCard/SubmissionCard.module.scss b/services/app-web/src/components/SubmissionCard/SubmissionCard.module.scss
index 5f556a9ef3..99c8b1b065 100644
--- a/services/app-web/src/components/SubmissionCard/SubmissionCard.module.scss
+++ b/services/app-web/src/components/SubmissionCard/SubmissionCard.module.scss
@@ -1,7 +1,6 @@
@use '../../styles/custom.scss' as custom;
@use '../../styles/uswdsImports.scss' as uswds;
-
.submissionList {
padding-left: 0;
}
@@ -16,7 +15,7 @@
transition: background-color 200ms linear;
&:hover {
- background-color: custom.$mcr-gray-lightest
+ background-color: custom.$mcr-gray-lightest;
}
}
diff --git a/services/app-web/src/components/SubmissionSummarySection/SubmissionSummarySection.module.scss b/services/app-web/src/components/SubmissionSummarySection/SubmissionSummarySection.module.scss
index 163f33d409..e080ad1289 100644
--- a/services/app-web/src/components/SubmissionSummarySection/SubmissionSummarySection.module.scss
+++ b/services/app-web/src/components/SubmissionSummarySection/SubmissionSummarySection.module.scss
@@ -8,9 +8,9 @@
.summarySection {
margin: uswds.units(2) auto;
background: custom.$mcr-foundation-white;
- padding: uswds.units(2)uswds.units(4);
+ padding: uswds.units(2) uswds.units(4);
border: 1px solid custom.$mcr-gray-lighter;
- line-height:uswds.units(3);
+ line-height: uswds.units(3);
h2 {
margin: 0;
@@ -28,7 +28,7 @@
padding: 0;
li {
- padding:uswds.units(1) 0;
+ padding: uswds.units(1) 0;
}
}
@@ -37,7 +37,7 @@
padding: 0;
div {
- padding-bottom:uswds.units(2);
+ padding-bottom: uswds.units(2);
}
// dont bottom pad last two grid items
@@ -46,7 +46,7 @@
}
}
table:last-of-type {
- margin-bottom:uswds.units(2);
+ margin-bottom: uswds.units(2);
}
// with nested sections, collapse bottom margin/padding for last in list
@@ -57,7 +57,7 @@
}
.contactInfo p {
- margin:uswds.units(1) 0;
+ margin: uswds.units(1) 0;
}
.documentDesc {
@@ -74,11 +74,11 @@
.rateName {
display: block;
font-weight: bold;
- margin: 0
+ margin: 0;
}
.certifyingActuaryDetail {
- margin-bottom:uswds.units(4);
+ margin-bottom: uswds.units(4);
@include uswds.at-media(tablet) {
margin-bottom: 0;
@@ -98,7 +98,7 @@
.singleColumnGrid {
@include uswds.at-media(tablet) {
> * {
- margin-bottom:uswds.units(2);
+ margin-bottom: uswds.units(2);
}
}
}
diff --git a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.module.scss b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.module.scss
index e916afda82..ba432ef0da 100644
--- a/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.module.scss
+++ b/services/app-web/src/components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.module.scss
@@ -4,7 +4,6 @@
@mixin docs-table {
width: 100%;
-
th {
font-size: uswds.size('body', '2xs');
}
@@ -22,7 +21,7 @@
}
}
.uploadedDocumentsTable {
- @include docs-table
+ @include docs-table;
}
.supportingDocsEmpty {
@@ -38,8 +37,8 @@
justify-content: space-between;
}
-.inlineLink{
- display: inline
+.inlineLink {
+ display: inline;
}
.inlineTag {
@@ -66,4 +65,3 @@ caption {
margin-right: 5px;
}
}
-
diff --git a/services/app-web/src/components/Tabs/Tabs.module.scss b/services/app-web/src/components/Tabs/Tabs.module.scss
index 2dfafeb352..139cd96afc 100644
--- a/services/app-web/src/components/Tabs/Tabs.module.scss
+++ b/services/app-web/src/components/Tabs/Tabs.module.scss
@@ -7,9 +7,7 @@
.easi-tabs {
overflow: hidden;
- height: 85%
-
- &__navigation {
+ height: 85% &__navigation {
display: flex;
justify-content: space-between;
position: relative;
@@ -85,5 +83,4 @@
padding: 1.5em;
min-height: 500px;
}
-
}
diff --git a/services/app-web/src/localAuth/LocalLogin.module.scss b/services/app-web/src/localAuth/LocalLogin.module.scss
index f078200c53..6aaf4dcae2 100644
--- a/services/app-web/src/localAuth/LocalLogin.module.scss
+++ b/services/app-web/src/localAuth/LocalLogin.module.scss
@@ -2,5 +2,5 @@
@use '../styles/uswdsImports.scss' as uswds;
.userCard {
- width: 200px;
+ width: 200px;
}
diff --git a/services/app-web/src/pages/App/AppBody.module.scss b/services/app-web/src/pages/App/AppBody.module.scss
index 692e2e7d66..44655ee15e 100644
--- a/services/app-web/src/pages/App/AppBody.module.scss
+++ b/services/app-web/src/pages/App/AppBody.module.scss
@@ -20,5 +20,5 @@
font-weight: bold;
text-align: center;
font-size: 1rem;
- padding: .5rem;
+ padding: 0.5rem;
}
diff --git a/services/app-web/src/pages/Errors/Errors.module.scss b/services/app-web/src/pages/Errors/Errors.module.scss
index 1b895bcedc..05f547bd1f 100644
--- a/services/app-web/src/pages/Errors/Errors.module.scss
+++ b/services/app-web/src/pages/Errors/Errors.module.scss
@@ -11,7 +11,7 @@
align-items: center;
}
-.errorsFullPage{
+.errorsFullPage {
flex: 1;
padding: uswds.units(4) 0;
}
diff --git a/services/app-web/src/pages/GraphQLExplorer/GraphQLExplorer.module.scss b/services/app-web/src/pages/GraphQLExplorer/GraphQLExplorer.module.scss
index 7b9e9d7ee1..19999560ee 100644
--- a/services/app-web/src/pages/GraphQLExplorer/GraphQLExplorer.module.scss
+++ b/services/app-web/src/pages/GraphQLExplorer/GraphQLExplorer.module.scss
@@ -2,14 +2,14 @@
@use '../../styles/uswdsImports.scss' as uswds;
.background {
- background-color: custom.$mcr-foundation-white;
- width: 100%;
- height: 100%;
- display: flex;
- flex: 1;
- align-items: stretch;
+ background-color: custom.$mcr-foundation-white;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex: 1;
+ align-items: stretch;
}
.explorer {
- width: 100%;
+ width: 100%;
}
diff --git a/services/app-web/src/pages/Help/Help.module.scss b/services/app-web/src/pages/Help/Help.module.scss
index bb497592f6..1ce8645c4b 100644
--- a/services/app-web/src/pages/Help/Help.module.scss
+++ b/services/app-web/src/pages/Help/Help.module.scss
@@ -5,7 +5,8 @@
margin: uswds.units(4) 0;
table {
- th, td {
+ th,
+ td {
vertical-align: top;
}
}
diff --git a/services/app-web/src/pages/Landing/Landing.module.scss b/services/app-web/src/pages/Landing/Landing.module.scss
index dec3795f7a..411f7435aa 100644
--- a/services/app-web/src/pages/Landing/Landing.module.scss
+++ b/services/app-web/src/pages/Landing/Landing.module.scss
@@ -1,7 +1,6 @@
@use '../../styles/custom.scss' as custom;
@use '../../styles/uswdsImports.scss' as uswds;
-
$details-text-line-height: 1.5;
.detailsSection {
@@ -68,6 +67,5 @@ $details-text-line-height: 1.5;
margin-bottom: uswds.units(1);
}
}
-
}
}
diff --git a/services/app-web/src/pages/MccrsId/MccrsId.module.scss b/services/app-web/src/pages/MccrsId/MccrsId.module.scss
index d7e59f3d3b..4a6ca64859 100644
--- a/services/app-web/src/pages/MccrsId/MccrsId.module.scss
+++ b/services/app-web/src/pages/MccrsId/MccrsId.module.scss
@@ -5,7 +5,7 @@
width: 100%;
}
-.mccrsIDForm {
+.mccrsIDForm {
[class^='usa-breadcrumb'] {
min-width: 40rem;
max-width: 20rem;
@@ -23,7 +23,6 @@
}
.formContainer.tableContainer {
-
&[class^='usa-form'] {
max-width: 100%;
width: 75rem;
@@ -33,7 +32,7 @@
.formHeader {
text-align: center;
width: 100%;
- padding:uswds.units(4) 0;
+ padding: uswds.units(4) 0;
}
.formContainer {
@@ -64,11 +63,10 @@
}
&[class^='usa-form'] {
-
min-width: 100%;
max-width: 100%;
- @include uswds.at-media(tablet){
+ @include uswds.at-media(tablet) {
min-width: 40rem;
max-width: 20rem;
margin: 0 auto;
@@ -92,10 +90,8 @@
button {
margin-top: 16px;
margin-bottom: 44px;
- margin-right: .25rem;
+ margin-right: 0.25rem;
}
}
}
-
}
-
diff --git a/services/app-web/src/pages/QuestionResponse/QATable/QATable.module.scss b/services/app-web/src/pages/QuestionResponse/QATable/QATable.module.scss
index be13ce7c01..0492d76636 100644
--- a/services/app-web/src/pages/QuestionResponse/QATable/QATable.module.scss
+++ b/services/app-web/src/pages/QuestionResponse/QATable/QATable.module.scss
@@ -1,22 +1,23 @@
@use '../../../styles/custom.scss' as custom;
@use '../../../styles/uswdsImports.scss' as uswds;
-@use '../../../components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.module.scss' as uploadedDocumentsTable;
+@use '../../../components/SubmissionSummarySection/UploadedDocumentsTable/UploadedDocumentsTable.module.scss'
+ as uploadedDocumentsTable;
.qaDocumentTable {
- @include uploadedDocumentsTable.docs-table;
- margin: 0 0 uswds.units(4) 0;
+ @include uploadedDocumentsTable.docs-table;
+ margin: 0 0 uswds.units(4) 0;
}
.tableHeader {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-bottom: uswds.units(3);
- h4 {
- padding: 0;
- margin: 0;
- }
- p {
- margin: 0;
- }
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: uswds.units(3);
+ h4 {
+ padding: 0;
+ margin: 0;
+ }
+ p {
+ margin: 0;
+ }
}
diff --git a/services/app-web/src/pages/QuestionResponse/QuestionResponse.module.scss b/services/app-web/src/pages/QuestionResponse/QuestionResponse.module.scss
index ed9b83ce77..517ce6a5e8 100644
--- a/services/app-web/src/pages/QuestionResponse/QuestionResponse.module.scss
+++ b/services/app-web/src/pages/QuestionResponse/QuestionResponse.module.scss
@@ -2,42 +2,42 @@
@use '../../styles/uswdsImports.scss' as uswds;
.background {
- background-color: custom.$mcr-foundation-white;
- width: 100%;
+ background-color: custom.$mcr-foundation-white;
+ width: 100%;
}
.container {
- max-width: custom.$mcr-container-standard-width-fixed;
- padding: 2rem 0;
+ max-width: custom.$mcr-container-standard-width-fixed;
+ padding: 2rem 0;
}
.questionSection {
- margin:uswds.units(2) auto;
- background: custom.$mcr-foundation-white;
- padding:uswds.units(4)uswds.units(4);
- border: 1px solid custom.$mcr-gray-lighter;
- line-height:uswds.units(3);
- width: 100%;
- @include uswds.u-radius('md');
+ margin: uswds.units(2) auto;
+ background: custom.$mcr-foundation-white;
+ padding: uswds.units(4) uswds.units(4);
+ border: 1px solid custom.$mcr-gray-lighter;
+ line-height: uswds.units(3);
+ width: 100%;
+ @include uswds.u-radius('md');
- h3 {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: uswds.units(2) 0;
- @include uswds.u-text('normal');
- }
+ h3 {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: uswds.units(2) 0;
+ @include uswds.u-text('normal');
+ }
}
.breadcrumbs {
- background: none;
- margin:uswds.units(2) auto;
+ background: none;
+ margin: uswds.units(2) auto;
}
.formContainer {
> [class^='usa-fieldset'] {
padding: uswds.units(4);
- margin-bottom:uswds.units(2);
- margin-top:uswds.units(2);
+ margin-bottom: uswds.units(2);
+ margin-top: uswds.units(2);
background: custom.$mcr-foundation-white;
border: 1px solid custom.$mcr-gray-lighter;
@include uswds.u-radius('md');
@@ -52,11 +52,10 @@
}
&[class^='usa-form'] {
-
min-width: 100%;
max-width: 100%;
- @include uswds.at-media(tablet){
+ @include uswds.at-media(tablet) {
min-width: 40rem;
max-width: 20rem;
margin: 0 auto;
diff --git a/services/app-web/src/pages/Settings/Settings.module.scss b/services/app-web/src/pages/Settings/Settings.module.scss
index 2a235f9848..bcd35cad68 100644
--- a/services/app-web/src/pages/Settings/Settings.module.scss
+++ b/services/app-web/src/pages/Settings/Settings.module.scss
@@ -2,11 +2,11 @@
@use '../../styles/uswdsImports.scss' as uswds;
.table {
- h2{
- font-weight: 300
+ h2 {
+ font-weight: 300;
}
- thead {
- th{
+ thead {
+ th {
background-color: transparent;
border-top: 0;
border-left: 0;
@@ -25,12 +25,12 @@
.pageContainer {
padding: 1em 10em;
width: 100%;
- background-color: custom.$mcr-foundation-white
+ background-color: custom.$mcr-foundation-white;
}
.header {
text-align: left;
}
.wrapper {
- height: 100vh;
+ height: 100vh;
}
diff --git a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.module.scss b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.module.scss
index ff1a863598..a31760c552 100644
--- a/services/app-web/src/pages/StateSubmission/StateSubmissionForm.module.scss
+++ b/services/app-web/src/pages/StateSubmission/StateSubmissionForm.module.scss
@@ -1,7 +1,6 @@
@use '../../styles/custom.scss' as custom;
@use '../../styles/uswdsImports.scss' as uswds;
-
.formPage {
width: 100%;
}
@@ -18,7 +17,7 @@
.formHeader {
text-align: center;
width: 100%;
- padding:uswds.units(4) 0;
+ padding: uswds.units(4) 0;
}
.formContainer {
@@ -31,7 +30,7 @@
@include uswds.u-radius('md');
}
// for supporting documents
- >& .tableContainer {
+ > & .tableContainer {
&[class^='usa-form'] {
max-width: 100%;
width: 75rem;
@@ -40,7 +39,7 @@
// the first fieldset of the form sets up form container
// in cases where form has multiple sub sections using SectionCard - use .withSections class
> [class^='usa-fieldset']:not([class~='with-sections']) {
- @include custom.sectionCard
+ @include custom.sectionCard;
}
> div[class^='usa-form-group']:not(:first-of-type) {
@@ -48,11 +47,10 @@
}
&[class^='usa-form'] {
-
min-width: 100%;
max-width: 100%;
- @include uswds.at-media(tablet){
+ @include uswds.at-media(tablet) {
min-width: 40rem;
max-width: 20rem;
margin: 0 auto;
@@ -135,11 +133,11 @@
.legendSubHeader {
font-weight: normal;
&.requiredOptionalText {
- margin-bottom:uswds.units(2);
+ margin-bottom: uswds.units(2);
}
}
-.guidanceTextBlock{
+.guidanceTextBlock {
padding-top: 0;
display: flex;
flex-direction: column;
@@ -150,7 +148,7 @@
color: custom.$mcr-foundation-hint;
}
-.guidanceTextBlockNoPadding{
+.guidanceTextBlockNoPadding {
display: flex;
flex-direction: column;
}
@@ -163,7 +161,7 @@
label {
max-width: none;
}
- div[role=note] {
+ div[role='note'] {
margin-top: uswds.units(0);
}
}
diff --git a/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.module.scss b/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.module.scss
index 3fe7c73d25..af32a133ba 100644
--- a/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.module.scss
+++ b/services/app-web/src/pages/SubmissionRevisionSummary/SubmissionRevisionSummary.module.scss
@@ -10,7 +10,7 @@
max-width: custom.$mcr-container-standard-width-fixed;
padding: 2rem 0;
- [id=submissionTypeSection] {
+ [id='submissionTypeSection'] {
[class^='SectionHeader_summarySectionHeader'] {
flex-direction: column;
align-items: flex-start;
diff --git a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.module.scss b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.module.scss
index 594d728c78..41fc13c3ec 100644
--- a/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.module.scss
+++ b/services/app-web/src/pages/SubmissionSideNav/SubmissionSideNav.module.scss
@@ -1,31 +1,29 @@
@use '../../styles/custom.scss' as custom;
@use '../../styles/uswdsImports.scss' as uswds;
-
.backgroundSidebar {
- background-color: custom.$mcr-foundation-white;
- width: 100%;
- height: 100%;
- flex: 1;
+ background-color: custom.$mcr-foundation-white;
+ width: 100%;
+ height: 100%;
+ flex: 1;
}
.backgroundForm {
- width: 100%;
+ width: 100%;
}
-
.container {
- @include custom.default-page-container;
- display: flex;
- flex-direction: row;
+ @include custom.default-page-container;
+ display: flex;
+ flex-direction: row;
}
.sideNavContainer {
- padding: 2rem 0;
- width: 20rem;
- max-width: custom.$mcr-container-max-width-fixed;
+ padding: 2rem 0;
+ width: 20rem;
+ max-width: custom.$mcr-container-max-width-fixed;
}
.backLinkContainer {
- padding-top: uswds.units(2);
- padding-bottom: uswds.units(4);
+ padding-top: uswds.units(2);
+ padding-bottom: uswds.units(4);
}
diff --git a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx
index 432096ceb3..b0555c5116 100644
--- a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx
+++ b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx
@@ -71,7 +71,10 @@ export const RateSummary = (): React.ReactElement => {
//TODO: Will have to remove this conditional along with associated loggedInUser prop once the rate dashboard
//is made available to state users
to={{
- pathname: loggedInUser?.__typename === 'StateUser' ? RoutesRecord.DASHBOARD : RoutesRecord.DASHBOARD_RATES,
+ pathname:
+ loggedInUser?.__typename === 'StateUser'
+ ? RoutesRecord.DASHBOARD
+ : RoutesRecord.DASHBOARD_RATES,
}}
>
diff --git a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.module.scss b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.module.scss
index 876726112b..ec7e00ac2e 100644
--- a/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.module.scss
+++ b/services/app-web/src/pages/SubmissionSummary/SubmissionSummary.module.scss
@@ -27,7 +27,6 @@
display: inline-block;
margin-left: 5px;
margin-top: 0px;
-
}
}
a.editLink {
@@ -42,7 +41,7 @@
.backLinkContainer {
padding-top: uswds.units(2);
padding-bottom: uswds.units(2);
- }
- .backLinkContainerNoSideBar {
+}
+.backLinkContainerNoSideBar {
padding-bottom: uswds.units(2);
- }
\ No newline at end of file
+}
diff --git a/services/app-web/src/styles/custom.scss b/services/app-web/src/styles/custom.scss
index a1e0f28f65..f2296c6e6e 100644
--- a/services/app-web/src/styles/custom.scss
+++ b/services/app-web/src/styles/custom.scss
@@ -9,13 +9,13 @@
@forward 'mcrColors'; // Allows access to colors anywhere custom is imported
@use 'mcrColors' as *;
-@use'uswdsImports.scss' as uswds;
+@use 'uswdsImports.scss' as uswds;
/* CONTAINERS */
// Every page starts with a flex container
@mixin container {
- display: flex;
- flex: 1 0 auto;
+ display: flex;
+ flex: 1 0 auto;
}
// We have some established width limits how far page content should stretch laterally. Right now this is controlled by CSS width properties
$mcr-container-standard-width-fixed: 50rem;
@@ -44,7 +44,8 @@ $mcr-container-max-width-fixed: 75rem;
$mcr-primary-dark,
$mcr-cyan-base
);
- box-shadow: inset 0 0 1px $mcr-gray-dark,
+ box-shadow:
+ inset 0 0 1px $mcr-gray-dark,
0px 0px 24px rgb(0 0 0 / 5%);
}
@@ -65,4 +66,4 @@ $mcr-container-max-width-fixed: 75rem;
width: 1px;
height: 1px;
overflow: hidden;
-}
\ No newline at end of file
+}
diff --git a/services/app-web/src/styles/mcrColors.scss b/services/app-web/src/styles/mcrColors.scss
index 9f44c0b857..38e1c8670f 100644
--- a/services/app-web/src/styles/mcrColors.scss
+++ b/services/app-web/src/styles/mcrColors.scss
@@ -6,46 +6,46 @@
*/
$mcr-primary-base: #005ea2; // USWDS 'primary'
-$mcr-primary-dark : #1a4480; // USWDS 'primary-dark'
-$mcr-primary-darkest: #162e51; // USWDS 'primary-darker',
+$mcr-primary-dark: #1a4480; // USWDS 'primary-dark'
+$mcr-primary-darkest: #162e51; // USWDS 'primary-darker',
$mcr-primary-light: #d9e8f6; // USWDS 'bg-primary-lighter',
$mcr-primary-lighter: #e1f3f8; // USWDS 'blue-cool-5v'
$mcr-cmsblue-base: #0071bc; // CMSDS 'color-primary'
$mcr-cmsblue-dark: #205493; // CMSDS 'color-primary-darker
$mcr-cmsblue-darkest: #112e51; // CMSDS 'color-primary-darkest'
-$mcr-cmsblue-lightest: #f0fafd;// This color is slightly off from CMSDS 'color-primary-alt-lightest', don't think it maps to either system
+$mcr-cmsblue-lightest: #f0fafd; // This color is slightly off from CMSDS 'color-primary-alt-lightest', don't think it maps to either system
$mcr-cyan-base: #02bfe7; // CMSDS 'color-primary-alt'
$mcr-cyan-dark: #009ec1; // USWDS 'cyan-40v'
-$mcr-cyan-light: #99deea;// USWDS 'cyan-20'
-$mcr-cyan-lighter: #e7f6F8; // USWDS 'cyan-5'
+$mcr-cyan-light: #99deea; // USWDS 'cyan-20'
+$mcr-cyan-lighter: #e7f6f8; // USWDS 'cyan-5'
$mcr-gold-lighter: #faf3d1; //USWDS yellow-5
-$mcr-gold-light: #fee685; // USWDS yellow 10v
+$mcr-gold-light: #fee685; // USWDS yellow 10v
$mcr-gold-base: #ffbe2e; // USWDS 'gold-20v'
$mcr-gold-dark: #e5a000; // USWDS 'gold-30v'
$mcr-gold-darker: #ca9318; // CMSDS 'color-warn-darker'
-$mcr-gray-base :#a9aeb1; // USWDS 'gray-cool-30'
+$mcr-gray-base: #a9aeb1; // USWDS 'gray-cool-30'
$mcr-gray-dark: #565c65; // USWDS 'gray-cool-60'
-$mcr-gray-lighter:#dfe1e2; // USWDS 'gray-cool-10'
+$mcr-gray-lighter: #dfe1e2; // USWDS 'gray-cool-10'
$mcr-gray-lightest: #f0f0f0; /// USWDS 'gray-5'
// mcr-foundation is used for text and containers
-$mcr-foundation-ink:#1b1b1b; // USWDS 'gray-90'
+$mcr-foundation-ink: #1b1b1b; // USWDS 'gray-90'
$mcr-foundation-hint: #71767a; // USWDS 'text-base'
$mcr-foundation-link: $mcr-primary-base;
-$mcr-foundation-focus:#3e94cf; // CMS 'color-focus'
+$mcr-foundation-focus: #3e94cf; // CMS 'color-focus'
$mcr-foundation-white: #fff;
$mcr-foundation-visited: #4c2c92; // CMS 'color-visited'
// mcr-success is used for submit buttons and completed actions
$mcr-success-base: #2e8540; // CMSDS 'color-success'
-$mcr-success-hover:#2a7a3b; // CMSDS 'color-success-dark" // CMSDS 'color-success-dark"
+$mcr-success-hover: #2a7a3b; // CMSDS 'color-success-dark" // CMSDS 'color-success-dark"
$mcr-success-dark: #4d8055; // USWDS 'green-cool-50'
// mcr-error is used for error, validations, and incomplete actions
$mcr-error-base: #b50909; // USWDS 'red-60v'
$mcr-error-dark: #981b1e; // CMS 'red-darkest
-$mcr-error-light: #f4e3db; // USWDS 'red-warm-10'
\ No newline at end of file
+$mcr-error-light: #f4e3db; // USWDS 'red-warm-10'
diff --git a/services/app-web/src/styles/overrides.scss b/services/app-web/src/styles/overrides.scss
index 7f763da417..1e01ed95dc 100644
--- a/services/app-web/src/styles/overrides.scss
+++ b/services/app-web/src/styles/overrides.scss
@@ -8,25 +8,25 @@
@use 'mcrColors' as *;
// FORM FIELDS
- .usa-label,
- .usa-legend {
- font-weight: bold;
- }
+.usa-label,
+.usa-legend {
+ font-weight: bold;
+}
- .usa-hint span {
- display: block;
- margin-top: 1rem;
- }
+.usa-hint span {
+ display: block;
+ margin-top: 1rem;
+}
- .usa-checkbox__label,
- .usa-radio__label {
- margin-top: 1rem;
- }
+.usa-checkbox__label,
+.usa-radio__label {
+ margin-top: 1rem;
+}
// TOOLTIP
// This can be removed removed when https://github.com/uswds/uswds/issues/4458 is fixed
.usa-tooltip__body {
- opacity: 0;
+ opacity: 0;
}
// BUTTONS
diff --git a/services/app-web/src/styles/theme/_color.scss b/services/app-web/src/styles/theme/_color.scss
index 0fcba3c1f2..f8b7e66b61 100644
--- a/services/app-web/src/styles/theme/_color.scss
+++ b/services/app-web/src/styles/theme/_color.scss
@@ -34,14 +34,12 @@ $theme-color-error-darker: $mcr-error-dark;
$theme-color-success: $mcr-success-base;
$theme-color-success-dark: $mcr-success-dark;
-
// Info colors
$theme-color-info-dark: $mcr-cyan-dark;
// Hint colors
$theme-color-hint: $mcr-foundation-hint;
-
/*
----------------------------------------
General colors
diff --git a/services/app-web/src/styles/uswdsSettings.scss b/services/app-web/src/styles/uswdsSettings.scss
index d123d3ebd7..bffc0fa875 100644
--- a/services/app-web/src/styles/uswdsSettings.scss
+++ b/services/app-web/src/styles/uswdsSettings.scss
@@ -30,4 +30,3 @@ $theme-footer-logos-background: #f0fafd;
/* MODAL */
$theme-modal-border-radius: 0;
-
diff --git a/services/infra-api/serverless.yml b/services/infra-api/serverless.yml
index d029562458..ec8742d32b 100644
--- a/services/infra-api/serverless.yml
+++ b/services/infra-api/serverless.yml
@@ -224,34 +224,19 @@ resources:
RoleArn: !GetAtt MetricStreamRole.Arn
OutputFormat: 'opentelemetry0.7'
- # AppApiGatewayAcl:
- # Type: AWS::WAFv2::WebACL
- # Properties:
- # DefaultAction:
- # Block: {}
- # Rules:
- # - Action:
- # Allow: {}
- # Name: ${sls:stage}-allow-usa-plus-territories
- # Priority: 0
- # Statement:
- # GeoMatchStatement:
- # CountryCodes:
- # - GU # Guam
- # - PR # Puerto Rico
- # - US # USA
- # - UM # US Minor Outlying Islands
- # - VI # US Virgin Islands
- # - MP # Northern Mariana Islands
- # VisibilityConfig:
- # SampledRequestsEnabled: true
- # CloudWatchMetricsEnabled: true
- # MetricName: WafWebAcl
- # Scope: REGIONAL
- # VisibilityConfig:
- # CloudWatchMetricsEnabled: true
- # SampledRequestsEnabled: true
- # MetricName: ${sls:stage}-webacl
+ JWTSecret:
+ Type: AWS::SecretsManager::Secret
+ Properties:
+ Name: 'api_jwt_secret_${sls:stage}'
+ Description: 'Dynamically generated secret for JWT signing/validation'
+ GenerateSecretString:
+ SecretStringTemplate: '{}'
+ GenerateStringKey: jwtsigningkey
+ PasswordLength: 128
+ ExcludePunctuation: true
+ ExcludeUppercase: true
+ ExcludeCharacters: 'ghijklmnopqrstuvwxyz' # we want to be generating a hex string [0-9a-f]
+ RequireEachIncludedType: false
Outputs:
ApiGatewayRestApiId:
From c1c7cb0258f11b92cab5ac753cb1f4afd32c21fa Mon Sep 17 00:00:00 2001
From: MacRae Linton <55759+macrael@users.noreply.github.com>
Date: Sun, 28 Jan 2024 22:35:24 -0800
Subject: [PATCH 03/24] Fix hard coded secret name that broke dev (#2214)
---
services/app-api/serverless.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/services/app-api/serverless.yml b/services/app-api/serverless.yml
index 76e4d5e5d0..7b419b7bcc 100644
--- a/services/app-api/serverless.yml
+++ b/services/app-api/serverless.yml
@@ -35,7 +35,7 @@ custom:
dbURL: ${env:DATABASE_URL}
ldSDKKey: ${env:LD_SDK_KEY, ssm:/configuration/ld_sdk_key_feds}
# because the secret is in JSON in secret manager, we have to pass it into jwtSecret when not running locally
- jwtSecretJSON: ${env:CF_CONFIG_IGNORED_LOCALLY, ssm:/aws/reference/secretsmanager/api_jwt_secret_wmltestapijwtaccess}
+ jwtSecretJSON: ${env:CF_CONFIG_IGNORED_LOCALLY, ssm:/aws/reference/secretsmanager/api_jwt_secret_${sls:stage}}
jwtSecret: ${env:JWT_SECRET, self:custom.jwtSecretJSON.jwtsigningkey}
webpack:
webpackConfig: 'webpack.config.js'
From e645afa4375f39ddc2bf746a7078ea9e18e82428 Mon Sep 17 00:00:00 2001
From: ruizajtruss <111928238+ruizajtruss@users.noreply.github.com>
Date: Mon, 29 Jan 2024 12:41:13 -0800
Subject: [PATCH 04/24] MCR-3793: state routing for unlocked rates (#2190)
* added edit rate route and constants
* placeholder and routing setup
* setup rerouting for :id
* setup redirect
* setup placeholder and some testing
* renamed feature flag
* updated routes
* components tweaked to redirect in a manner more inline with established patterns
* updated tests
* adjusted for launchdarkly update
* remove unused param
* removed unnecessary spacing
---
services/app-web/src/constants/routes.ts | 5 +-
services/app-web/src/constants/tealium.ts | 1 +
services/app-web/src/pages/App/AppRoutes.tsx | 11 +++-
.../src/pages/RateEdit/RateEdit.test.tsx | 48 +++++++++++++++++
.../app-web/src/pages/RateEdit/RateEdit.tsx | 52 +++++++++++++++++++
.../RateSummary/RateSummary.test.tsx | 38 +++++++++++++-
.../RateSummary/RateSummary.tsx | 11 ++--
7 files changed, 159 insertions(+), 7 deletions(-)
create mode 100644 services/app-web/src/pages/RateEdit/RateEdit.test.tsx
create mode 100644 services/app-web/src/pages/RateEdit/RateEdit.tsx
diff --git a/services/app-web/src/constants/routes.ts b/services/app-web/src/constants/routes.ts
index 22e98b4ded..747b7b8875 100644
--- a/services/app-web/src/constants/routes.ts
+++ b/services/app-web/src/constants/routes.ts
@@ -12,6 +12,7 @@ const ROUTES = [
'HELP',
'SETTINGS',
'RATES_SUMMARY',
+ 'RATE_EDIT',
'SUBMISSIONS',
'SUBMISSIONS_NEW',
'SUBMISSIONS_TYPE',
@@ -46,7 +47,8 @@ const RoutesRecord: Record = {
GRAPHQL_EXPLORER: '/dev/graphql-explorer',
HELP: '/help',
SETTINGS: '/settings',
- RATES_SUMMARY: 'rates/:id',
+ RATES_SUMMARY: '/rates/:id',
+ RATE_EDIT: '/rates/:id/edit',
SUBMISSIONS: '/submissions',
SUBMISSIONS_NEW: '/submissions/new',
SUBMISSIONS_EDIT_TOP_LEVEL: '/submissions/:id/edit/*',
@@ -122,6 +124,7 @@ const PageTitlesRecord: Record = {
DASHBOARD_RATES: 'Rate review dashboard',
DASHBOARD_SUBMISSIONS: 'Dashboard',
RATES_SUMMARY: 'Rate summary',
+ RATE_EDIT: 'Edit rate',
SUBMISSIONS: 'Submissions',
SUBMISSIONS_NEW: 'New submission',
SUBMISSIONS_EDIT_TOP_LEVEL: 'Submissions',
diff --git a/services/app-web/src/constants/tealium.ts b/services/app-web/src/constants/tealium.ts
index 02dbad6468..030a7659ff 100644
--- a/services/app-web/src/constants/tealium.ts
+++ b/services/app-web/src/constants/tealium.ts
@@ -38,6 +38,7 @@ const CONTENT_TYPE_BY_ROUTE: Record = {
GRAPHQL_EXPLORER: 'dev',
SETTINGS: 'table',
RATES_SUMMARY: 'summary',
+ RATE_EDIT: 'form',
SUBMISSIONS: 'form',
SUBMISSIONS_NEW: 'form',
SUBMISSIONS_EDIT_TOP_LEVEL: 'form',
diff --git a/services/app-web/src/pages/App/AppRoutes.tsx b/services/app-web/src/pages/App/AppRoutes.tsx
index 81f1b33d42..58b1a9053f 100644
--- a/services/app-web/src/pages/App/AppRoutes.tsx
+++ b/services/app-web/src/pages/App/AppRoutes.tsx
@@ -38,6 +38,7 @@ import {
} from '../QuestionResponse'
import { GraphQLExplorer } from '../GraphQLExplorer/GraphQLExplorer'
import { RateSummary } from '../SubmissionSummary/RateSummary'
+import { RateEdit } from '../RateEdit/RateEdit'
function componentForAuthMode(
authMode: AuthModeType
@@ -75,7 +76,7 @@ const StateUserRoutes = ({
}): React.ReactElement => {
// feature flag
const ldClient = useLDClient()
- const showRateSummaryPage: boolean = ldClient?.variation(
+ const showRatePages: boolean = ldClient?.variation(
featureFlags.RATE_EDIT_UNLOCK.flag,
featureFlags.RATE_EDIT_UNLOCK.defaultValue
)
@@ -106,7 +107,13 @@ const StateUserRoutes = ({
path={RoutesRecord.SUBMISSIONS_NEW}
element={}
/>
- {showRateSummaryPage && (
+ {showRatePages && (
+ }
+ />
+ )}
+ {showRatePages && (
}
diff --git a/services/app-web/src/pages/RateEdit/RateEdit.test.tsx b/services/app-web/src/pages/RateEdit/RateEdit.test.tsx
new file mode 100644
index 0000000000..420d69c305
--- /dev/null
+++ b/services/app-web/src/pages/RateEdit/RateEdit.test.tsx
@@ -0,0 +1,48 @@
+import { screen, waitFor } from '@testing-library/react'
+import { renderWithProviders } from "../../testHelpers"
+import { RateEdit } from "./RateEdit"
+import { fetchCurrentUserMock, fetchRateMockSuccess, mockValidStateUser } from "../../testHelpers/apolloMocks"
+import { RoutesRecord } from '../../constants'
+import { Route, Routes } from 'react-router-dom'
+
+// Wrap test component in some top level routes to allow getParams to be tested
+const wrapInRoutes = (children: React.ReactNode) => {
+ return (
+
+
+
+ )
+}
+
+describe('RateEdit', () => {
+ afterAll(() => jest.clearAllMocks())
+
+ describe('Viewing RateEdit as a state user', () => {
+
+ it('renders without errors', async () => {
+ renderWithProviders(wrapInRoutes(), {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidStateUser(),
+ statusCode: 200,
+ }),
+ fetchRateMockSuccess({ rate: { id: '1337', status: 'UNLOCKED' } }),
+ ],
+ },
+ routerProvider: {
+ route: '/rates/1337/edit'
+ },
+ featureFlags: {
+ 'rate-edit-unlock': true
+ }
+ })
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('rate-edit')).toBeInTheDocument()
+ })
+ })
+ })
+})
+
+
diff --git a/services/app-web/src/pages/RateEdit/RateEdit.tsx b/services/app-web/src/pages/RateEdit/RateEdit.tsx
new file mode 100644
index 0000000000..af04cbfb00
--- /dev/null
+++ b/services/app-web/src/pages/RateEdit/RateEdit.tsx
@@ -0,0 +1,52 @@
+import React from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { useFetchRateQuery } from "../../gen/gqlClient";
+import { GridContainer } from "@trussworks/react-uswds";
+import { Loading } from "../../components";
+import { GenericErrorPage } from "../Errors/GenericErrorPage";
+
+type RouteParams = {
+ id: string
+}
+
+export const RateEdit = (): React.ReactElement => {
+ const navigate = useNavigate()
+ const { id } = useParams()
+ if (!id) {
+ throw new Error(
+ 'PROGRAMMING ERROR: id param not set in state submission form.'
+ )
+ }
+
+ const { data, loading, error } = useFetchRateQuery({
+ variables: {
+ input: {
+ rateID: id,
+ },
+ },
+ })
+
+ const rate = data?.fetchRate.rate
+
+ if (loading) {
+ return (
+
+
+
+ )
+ } else if (error || !rate ) {
+ return
+ }
+
+ if (rate.status !== 'UNLOCKED') {
+ navigate(`/rates/${id}`)
+ }
+
+ return (
+
+ You've reached the '/rates/:id/edit' url placeholder for the incoming standalone edit rate form
+
+ Ticket: MCR-3771
+
+ )
+}
\ No newline at end of file
diff --git a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx
index 75ae0e088c..490f69e143 100644
--- a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx
+++ b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.test.tsx
@@ -9,6 +9,7 @@ import {
import { RateSummary } from './RateSummary'
import { RoutesRecord } from '../../../constants'
import { Route, Routes } from 'react-router-dom'
+import { RateEdit } from '../../RateEdit/RateEdit'
// Wrap test component in some top level routes to allow getParams to be tested
const wrapInRoutes = (children: React.ReactNode) => {
@@ -114,7 +115,7 @@ describe('RateSummary', () => {
})
describe('Viewing RateSummary as a State user', () => {
- it('renders without errors', async () => {
+ it('renders SingleRateSummarySection component without errors for locked rate', async () => {
renderWithProviders(wrapInRoutes(), {
apolloProvider: {
mocks: [
@@ -142,6 +143,41 @@ describe('RateSummary', () => {
).toBeInTheDocument()
})
+ it('redirects to RateEdit component from RateSummary without errors for unlocked rate', async () => {
+ renderWithProviders(
+
+ }
+ />
+ }
+ />
+ ,
+ {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidStateUser(),
+ statusCode: 200,
+ }),
+ fetchRateMockSuccess({ rate: { id: '1337', status: 'UNLOCKED' } }),
+ ],
+ },
+ routerProvider: {
+ route: '/rates/1337'
+ },
+ featureFlags: {
+ 'rate-edit-unlock': true
+ }
+ })
+
+ await waitFor(() => {
+ expect(screen.queryByTestId('rate-edit')).toBeInTheDocument()
+ })
+ })
+
it('renders expected error page when rate ID is invalid', async () => {
renderWithProviders(wrapInRoutes(), {
apolloProvider: {
diff --git a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx
index b0555c5116..fc0855602f 100644
--- a/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx
+++ b/services/app-web/src/pages/SubmissionSummary/RateSummary/RateSummary.tsx
@@ -1,6 +1,6 @@
import { GridContainer, Icon, Link } from '@trussworks/react-uswds'
import React, { useEffect, useState } from 'react'
-import { NavLink, useParams } from 'react-router-dom'
+import { NavLink, useNavigate, useParams } from 'react-router-dom'
import { Loading } from '../../../components'
import { usePage } from '../../../contexts/PageContext'
@@ -19,6 +19,7 @@ export const RateSummary = (): React.ReactElement => {
// Page level state
const { loggedInUser } = useAuth()
const { updateHeading } = usePage()
+ const navigate = useNavigate()
const [rateName, setRateName] = useState(undefined)
const { id } = useParams()
if (!id) {
@@ -52,6 +53,11 @@ export const RateSummary = (): React.ReactElement => {
return
}
+ //Redirecting a state user to the edit page if rate is unlocked
+ if (loggedInUser?.role === 'STATE_USER' && rate.status === 'UNLOCKED') {
+ navigate(`/rates/${id}/edit`)
+ }
+
if (
rateName !== currentRateRev.formData.rateCertificationName &&
currentRateRev.formData.rateCertificationName
@@ -68,8 +74,7 @@ export const RateSummary = (): React.ReactElement => {
Date: Mon, 29 Jan 2024 18:57:30 -0500
Subject: [PATCH 05/24] Revert clamdscan layer PR (#2216)
* PR revert JIC
* sha dance
* remove the dl
---
.github/workflows/deploy-infra-to-env.yml | 10 -
.github/workflows/deploy.yml | 25 +--
.github/workflows/promote.yml | 23 +-
services/uploads/serverless.yml | 7 +-
services/uploads/src/avLayer/LICENSE | 201 ------------------
services/uploads/src/avLayer/build/build.sh | 46 ----
.../uploads/src/avLayer/build/freshclam.conf | 2 -
.../uploads/src/avLayer/docker-compose.yml | 9 -
services/uploads/src/avLayer/dockerbuild.sh | 5 -
9 files changed, 9 insertions(+), 319 deletions(-)
delete mode 100644 services/uploads/src/avLayer/LICENSE
delete mode 100755 services/uploads/src/avLayer/build/build.sh
delete mode 100644 services/uploads/src/avLayer/build/freshclam.conf
delete mode 100644 services/uploads/src/avLayer/docker-compose.yml
delete mode 100755 services/uploads/src/avLayer/dockerbuild.sh
diff --git a/.github/workflows/deploy-infra-to-env.yml b/.github/workflows/deploy-infra-to-env.yml
index 06e86948b3..91b9ca98cf 100644
--- a/.github/workflows/deploy-infra-to-env.yml
+++ b/.github/workflows/deploy-infra-to-env.yml
@@ -172,16 +172,6 @@ jobs:
stage-name: ${{ inputs.stage_name }}
changed-services: ${{ inputs.changed_services }}
- - uses: actions/download-artifact@v3
- with:
- name: lambda-layers-clamav
- path: ./services/uploads/lambda-layers-clamav
-
- - name: Unzip clamav layer
- run: |
- unzip -d ./services/uploads/lambda-layers-clamav ./services/uploads/lambda-layers-clamav/lambda_layer.zip
- rm -f ./services/uploads/lambda-layers-clamav/lambda_layer.zip
-
- name: deploy uploads
id: deploy-uploads
env:
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index bd0b2f4839..3516b53c13 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -249,30 +249,9 @@ jobs:
name: lambda-layers-prisma-client-engine
path: ./services/app-api/lambda-layers-prisma-client-engine
- build-clamav-layer:
- name: build - clamav layer
- runs-on: ubuntu-20.04
- steps:
- - name: Check out repository
- uses: actions/checkout@v4
-
- - uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
- cache: yarn
-
- - name: Prepare ClamAV layer
- working-directory: services/uploads/src/avLayer
- run: ./dockerbuild.sh
-
- - uses: actions/upload-artifact@v3
- with:
- name: lambda-layers-clamav
- path: ./services/uploads/src/avLayer/build/lambda_layer.zip
-
deploy-infra:
- needs: [begin-deployment, build-clamav-layer]
- uses: Enterprise-CMCS/managed-care-review/.github/workflows/deploy-infra-to-env.yml@main
+ needs: [begin-deployment]
+ uses: Enterprise-CMCS/managed-care-review/.github/workflows/deploy-infra-to-env.yml@mt-revert-clamav-pr
with:
environment: dev
stage_name: ${{ needs.begin-deployment.outputs.stage-name}}
diff --git a/.github/workflows/promote.yml b/.github/workflows/promote.yml
index 98bc3bed0f..7be91378a8 100644
--- a/.github/workflows/promote.yml
+++ b/.github/workflows/promote.yml
@@ -117,29 +117,8 @@ jobs:
name: lambda-layers-prisma-client-engine
path: ./services/app-api/lambda-layers-prisma-client-engine
- build-clamav-layer:
- name: build - clamav layer
- runs-on: ubuntu-20.04
- steps:
- - name: Check out repository
- uses: actions/checkout@v4
-
- - uses: actions/setup-node@v4
- with:
- node-version-file: '.nvmrc'
- cache: yarn
-
- - name: Prepare ClamAV layer
- working-directory: services/uploads/src/avLayer
- run: ./dockerbuild.sh
-
- - uses: actions/upload-artifact@v3
- with:
- name: lambda-layers-clamav
- path: ./services/uploads/src/avLayer/build/lambda_layer.zip
-
promote-infra-dev:
- needs: [build-prisma-client-lambda-layer, build-clamav-layer, unit-tests]
+ needs: [build-prisma-client-lambda-layer, unit-tests]
uses: Enterprise-CMCS/managed-care-review/.github/workflows/deploy-infra-to-env.yml@main
with:
environment: dev
diff --git a/services/uploads/serverless.yml b/services/uploads/serverless.yml
index 5964a08a7c..0a59aee6f2 100644
--- a/services/uploads/serverless.yml
+++ b/services/uploads/serverless.yml
@@ -60,7 +60,11 @@ custom:
scripts:
hooks:
# This script is run locally when running 'serverless deploy'
+ package:initialize: |
+ set -e
+ curl -L --output lambda_layer.zip https://github.com/CMSgov/lambda-clamav-layer/releases/download/0.7/lambda_layer.zip
deploy:finalize: |
+ rm lambda_layer.zip
serverless invoke --stage ${sls:stage} --function avDownloadDefinitions -t Event
serverless-offline-ssm:
stages:
@@ -92,7 +96,8 @@ custom:
layers:
clamAv:
- path: lambda-layers-clamav
+ package:
+ artifact: lambda_layer.zip
functions:
avScan:
diff --git a/services/uploads/src/avLayer/LICENSE b/services/uploads/src/avLayer/LICENSE
deleted file mode 100644
index 261eeb9e9f..0000000000
--- a/services/uploads/src/avLayer/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/services/uploads/src/avLayer/build/build.sh b/services/uploads/src/avLayer/build/build.sh
deleted file mode 100755
index 7e337b470e..0000000000
--- a/services/uploads/src/avLayer/build/build.sh
+++ /dev/null
@@ -1,46 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-
-VERSION=${VERSION:-0.104.4-27025}
-echo "prepping clamav (${VERSION})"
-uname -m
-
-yum update -y
-amazon-linux-extras install epel -y
-yum install -y cpio yum-utils tar.x86_64 gzip zip
-
-# extract binaries for clamav, json-c, pcre
-mkdir -p /tmp/build
-pushd /tmp/build
-
-# Download other package dependencies
-yumdownloader -x \*i686 --archlist=x86_64 clamav clamav-lib clamav-update clamd json-c pcre2 libtool-ltdl libxml2 bzip2-libs xz-libs libprelude gnutls nettle
-rpm2cpio clamav-0*.rpm | cpio -vimd
-rpm2cpio clamav-lib*.rpm | cpio -vimd
-rpm2cpio clamav-update*.rpm | cpio -vimd
-rpm2cpio clamd*.rpm | cpio -vimd
-rpm2cpio json-c*.rpm | cpio -vimd
-rpm2cpio pcre*.rpm | cpio -vimd
-rpm2cpio libtool-ltdl*.rpm | cpio -vimd
-rpm2cpio libxml2*.rpm | cpio -vimd
-rpm2cpio bzip2-libs*.rpm | cpio -vimd
-rpm2cpio xz-libs*.rpm | cpio -vimd
-rpm2cpio libprelude*.rpm | cpio -vimd
-rpm2cpio gnutls*.rpm | cpio -vimd
-rpm2cpio nettle*.rpm | cpio -vimd
-
-# reset the timestamps so that we generate a reproducible zip file where
-# running with the same file contents we get the exact same hash even if we
-# run the same build on different days
-find usr -exec touch -t 200001010000 "{}" \;
-popd
-
-mkdir -p bin lib
-
-cp /tmp/build/usr/bin/clamscan /tmp/build/usr/bin/freshclam /tmp/build/usr/bin/clamdscan bin/.
-cp -R /tmp/build/usr/lib64/* lib/.
-cp freshclam.conf bin/freshclam.conf
-
-zip -r9 lambda_layer.zip bin
-zip -r9 lambda_layer.zip lib
\ No newline at end of file
diff --git a/services/uploads/src/avLayer/build/freshclam.conf b/services/uploads/src/avLayer/build/freshclam.conf
deleted file mode 100644
index b2e6fbf18e..0000000000
--- a/services/uploads/src/avLayer/build/freshclam.conf
+++ /dev/null
@@ -1,2 +0,0 @@
-DatabaseMirror database.clamav.net
-CompressLocalDatabase yes
diff --git a/services/uploads/src/avLayer/docker-compose.yml b/services/uploads/src/avLayer/docker-compose.yml
deleted file mode 100644
index 51f2388b41..0000000000
--- a/services/uploads/src/avLayer/docker-compose.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-version: '2'
-services:
- layer:
- image: amazonlinux:latest
- platform: linux/amd64
- working_dir: /opt/app
- volumes:
- - ./build:/opt/app
- command: [./build.sh]
diff --git a/services/uploads/src/avLayer/dockerbuild.sh b/services/uploads/src/avLayer/dockerbuild.sh
deleted file mode 100755
index 5e404f3fa7..0000000000
--- a/services/uploads/src/avLayer/dockerbuild.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env bash
-
-set -e
-docker pull amazonlinux:2
-docker run --rm --platform linux/x86_64 -v `pwd`/build:/opt/app amazonlinux:2 /bin/bash -c "cd /opt/app && ./build.sh"
\ No newline at end of file
From aab6616db3aec4bcb4b4ad88c5dd36031aaf0a1d Mon Sep 17 00:00:00 2001
From: Mojo Talantikite
Date: Mon, 29 Jan 2024 20:11:20 -0500
Subject: [PATCH 06/24] move the sha (#2217)
---
.github/workflows/deploy.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 3516b53c13..91164a872a 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -251,7 +251,7 @@ jobs:
deploy-infra:
needs: [begin-deployment]
- uses: Enterprise-CMCS/managed-care-review/.github/workflows/deploy-infra-to-env.yml@mt-revert-clamav-pr
+ uses: Enterprise-CMCS/managed-care-review/.github/workflows/deploy-infra-to-env.yml@main
with:
environment: dev
stage_name: ${{ needs.begin-deployment.outputs.stage-name}}
From f6a6d68043dc61a68efd1815055ea8e0e044ea93 Mon Sep 17 00:00:00 2001
From: pearl-truss <67110378+pearl-truss@users.noreply.github.com>
Date: Wed, 31 Jan 2024 08:31:14 -0500
Subject: [PATCH 07/24] MCR-3836 - Modify app-api to process request from the
3rd party authorizer (#2171)
* user the principalID passed back from authorizer to fetch user
* simplify localAuthn to not contain 3rd party authorizer logic
* resolve issue with deployed environment not having an authprovider for 3rd party requests
* make sure normal userFetcher isn't called when request comes from authroizer
* testing commit to check if the path exist on the event in the same way in deployed
* additional output in thrown error for testing
* update logic for checking if request is coming from 3rd party
* pr fixes for better error message and remove duplicate code
* fix typo
* test awaiting for userID from jwtLib
* Add documentation for third party access to API
* throw error
* remove changes to authn that are no longer needed
* update error handling, alert with otel
* resolve issue with apollo_gql expecting error returned for userfetcher
---
.../third_party_api_access.md | 16 ++++++++
services/app-api/src/authn/index.ts | 1 +
services/app-api/src/authn/thirdPartyAuthn.ts | 39 +++++++++++++++++++
services/app-api/src/handlers/apollo_gql.ts | 30 +++++++++++---
4 files changed, 81 insertions(+), 5 deletions(-)
create mode 100644 docs/technical-design/third_party_api_access.md
create mode 100644 services/app-api/src/authn/thirdPartyAuthn.ts
diff --git a/docs/technical-design/third_party_api_access.md b/docs/technical-design/third_party_api_access.md
new file mode 100644
index 0000000000..a13487aa3d
--- /dev/null
+++ b/docs/technical-design/third_party_api_access.md
@@ -0,0 +1,16 @@
+# Third Party API Access
+
+## Overview
+In order to make a request to the API, users (3rd party and non) need to be authenticated (signed in) and authorized (have permission to make a particular request). Non 3rd party users are both authenticated and authorized with Cognito. While 3rd party users will be able to authenticate with cognito, authorization will happen separately that we can grant them longer term credentials for authorization.
+
+## Process for 3rd Parties to Use the MC-Review API
+
+1. 3rd party is authenticated (signs on)
+2. 3rd party requests a JWT
+3. 3rd party uses the JWT to make a request to the MC-Review API
+4. Request is sent to `v1/graphql/external` via the `graphql` API gateway endpoint, which invokes the lambda authorizer
+5. The lambda authorizer, `third_party_api_authorizer`, verifies the JWT that the 3rd party sent with their request. If the JWT is valid (valid user, and not expired) the lambda returns an “allow” policy document, otherwise it returns a “deny”. This policy determines if the request can proceed.
+6. When authorization is successful the user ID that was granted the JWT is used to fetch a full user record from postgres. This is user is then a part of the context for the resolver.
+
+## JWT Security
+Like previously mentioned, third parties will need to have a valid JWT in order to access the MC-Review API. More can be found on JWT security [here](api-jwt-security.md)
\ No newline at end of file
diff --git a/services/app-api/src/authn/index.ts b/services/app-api/src/authn/index.ts
index 485ebb656a..c7af248951 100644
--- a/services/app-api/src/authn/index.ts
+++ b/services/app-api/src/authn/index.ts
@@ -1,6 +1,7 @@
export type { userFromAuthProvider } from './authn'
export { userFromCognitoAuthProvider, lookupUserAurora } from './cognitoAuthn'
+export { userFromThirdPartyAuthorizer } from './thirdPartyAuthn'
export {
userFromLocalAuthProvider,
diff --git a/services/app-api/src/authn/thirdPartyAuthn.ts b/services/app-api/src/authn/thirdPartyAuthn.ts
new file mode 100644
index 0000000000..d8c5c8923d
--- /dev/null
+++ b/services/app-api/src/authn/thirdPartyAuthn.ts
@@ -0,0 +1,39 @@
+import { ok, err } from 'neverthrow'
+import type { Store } from '../postgres'
+import { lookupUserAurora } from './cognitoAuthn'
+import { initTracer, recordException } from '../../../uploads/src/lib/otel'
+
+export async function userFromThirdPartyAuthorizer(
+ store: Store,
+ userId: string
+) {
+ // setup otel tracing
+ const otelCollectorURL = process.env.REACT_APP_OTEL_COLLECTOR_URL
+ if (!otelCollectorURL || otelCollectorURL === '') {
+ const errMsg =
+ 'Configuration Error: REACT_APP_OTEL_COLLECTOR_URL must be set'
+ throw errMsg
+ }
+
+ const serviceName = 'third-party-authorizer'
+ initTracer(serviceName, otelCollectorURL)
+
+ try {
+ // Lookup user from postgres
+ const auroraUser = await lookupUserAurora(store, userId)
+ if (auroraUser instanceof Error) {
+ return err(auroraUser)
+ }
+
+ if (auroraUser === undefined) {
+ return err(auroraUser)
+ }
+
+ return ok(auroraUser)
+ } catch (e) {
+ const err = new Error('ERROR: failed to look up user in postgres')
+
+ recordException(err, serviceName, 'lookupUser')
+ throw err
+ }
+}
diff --git a/services/app-api/src/handlers/apollo_gql.ts b/services/app-api/src/handlers/apollo_gql.ts
index 1377e56099..0e675f6f42 100644
--- a/services/app-api/src/handlers/apollo_gql.ts
+++ b/services/app-api/src/handlers/apollo_gql.ts
@@ -13,6 +13,7 @@ import type { userFromAuthProvider } from '../authn'
import {
userFromCognitoAuthProvider,
userFromLocalAuthProvider,
+ userFromThirdPartyAuthorizer,
} from '../authn'
import { newLocalEmailer, newSESEmailer } from '../emailer'
import { NewPostgresStore } from '../postgres/postgresStore'
@@ -59,10 +60,17 @@ function contextForRequestForFetcher(userFetcher: userFromAuthProvider): ({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const anyContext = context as any
const requestSpan = anyContext[requestSpanKey]
-
const authProvider =
event.requestContext.identity.cognitoAuthenticationProvider
- if (authProvider) {
+ // This handler is shared with the third_party_API_authorizer
+ // when called from the 3rd party authorizer the cognito auth provider
+ // is not valid for instead the authorizer returns a user ID
+ // that is used to fetch the user
+ const fromThirdPartyAuthorizer = event.requestContext.path.includes(
+ '/v1/graphql/external'
+ )
+
+ if (authProvider || fromThirdPartyAuthorizer) {
try {
// check if the user is stored in postgres
// going to clean this up, but we need the store in the
@@ -81,8 +89,21 @@ function contextForRequestForFetcher(userFetcher: userFromAuthProvider): ({
}
const store = NewPostgresStore(pgResult)
- const userResult = await userFetcher(authProvider, store)
+ const userId = event.requestContext.authorizer?.principalId
+
+ let userResult
+ if (authProvider && !fromThirdPartyAuthorizer) {
+ userResult = await userFetcher(authProvider, store)
+ } else if (fromThirdPartyAuthorizer && userId) {
+ userResult = await userFromThirdPartyAuthorizer(
+ store,
+ userId
+ )
+ }
+ if (userResult === undefined) {
+ throw new Error(`Log: userResult must be supplied`)
+ }
if (!userResult.isErr()) {
return {
user: userResult.value,
@@ -98,7 +119,7 @@ function contextForRequestForFetcher(userFetcher: userFromAuthProvider): ({
throw new Error('Log: placing user in gql context failed')
}
} else {
- throw new Error('Log: no AuthProvider')
+ throw new Error('Log: no AuthProvider from an internal API user.')
}
}
}
@@ -132,7 +153,6 @@ function tracingMiddleware(wrapped: Handler): Handler {
return async function (event, context, completion) {
// get the parent context from headers
const ctx = propagation.extract(ROOT_CONTEXT, event.headers)
-
const span = tracer.startSpan(
'handleRequest',
{
From 5078efd421a7387f51ed54a931f26041e3d88785 Mon Sep 17 00:00:00 2001
From: MacRae Linton <55759+macrael@users.noreply.github.com>
Date: Wed, 31 Jan 2024 14:55:03 -0800
Subject: [PATCH 08/24] Add API Key Generation Page (#2218)
* initial token generation page
* get api key page working
---
services/app-web/src/constants/routes.ts | 3 +
services/app-web/src/constants/tealium.ts | 1 +
.../app-web/src/gqlHelpers/apolloErrors.ts | 2 +-
services/app-web/src/index.tsx | 7 +-
.../src/pages/APIAccess/APIAccess.module.scss | 22 +++
.../src/pages/APIAccess/APIAccess.test.tsx | 109 ++++++++++++
.../app-web/src/pages/APIAccess/APIAccess.tsx | 155 ++++++++++++++++++
services/app-web/src/pages/App/AppRoutes.tsx | 3 +
.../src/pages/RateEdit/RateEdit.test.tsx | 73 +++++----
.../app-web/src/pages/RateEdit/RateEdit.tsx | 84 +++++-----
.../SubmissionSideNav/SubmissionSideNav.tsx | 1 -
.../RateSummary/RateSummary.test.tsx | 29 ++--
.../testHelpers/apolloMocks/apiKeyGQLMocks.ts | 33 ++++
.../src/testHelpers/apolloMocks/index.ts | 5 +
.../thirdPartyAPIAccess.spec.ts | 78 +++++++++
15 files changed, 513 insertions(+), 92 deletions(-)
create mode 100644 services/app-web/src/pages/APIAccess/APIAccess.module.scss
create mode 100644 services/app-web/src/pages/APIAccess/APIAccess.test.tsx
create mode 100644 services/app-web/src/pages/APIAccess/APIAccess.tsx
create mode 100644 services/app-web/src/testHelpers/apolloMocks/apiKeyGQLMocks.ts
create mode 100644 services/cypress/integration/thirdPartyAPIAccess/thirdPartyAPIAccess.spec.ts
diff --git a/services/app-web/src/constants/routes.ts b/services/app-web/src/constants/routes.ts
index 747b7b8875..890ca59c97 100644
--- a/services/app-web/src/constants/routes.ts
+++ b/services/app-web/src/constants/routes.ts
@@ -9,6 +9,7 @@ const ROUTES = [
'DASHBOARD_SUBMISSIONS',
'DASHBOARD_RATES',
'GRAPHQL_EXPLORER',
+ 'API_ACCESS',
'HELP',
'SETTINGS',
'RATES_SUMMARY',
@@ -45,6 +46,7 @@ const RoutesRecord: Record = {
DASHBOARD_SUBMISSIONS: '/dashboard/submissions',
DASHBOARD_RATES: '/dashboard/rate-reviews',
GRAPHQL_EXPLORER: '/dev/graphql-explorer',
+ API_ACCESS: '/dev/api-access',
HELP: '/help',
SETTINGS: '/settings',
RATES_SUMMARY: '/rates/:id',
@@ -118,6 +120,7 @@ const PageTitlesRecord: Record = {
ROOT: 'Home',
AUTH: 'Login',
GRAPHQL_EXPLORER: 'GraphQL explorer',
+ API_ACCESS: 'API Access',
HELP: 'Help',
SETTINGS: 'Settings',
DASHBOARD: 'Dashboard',
diff --git a/services/app-web/src/constants/tealium.ts b/services/app-web/src/constants/tealium.ts
index 030a7659ff..4aaf4ad0b1 100644
--- a/services/app-web/src/constants/tealium.ts
+++ b/services/app-web/src/constants/tealium.ts
@@ -36,6 +36,7 @@ const CONTENT_TYPE_BY_ROUTE: Record = {
DASHBOARD_RATES: 'table',
HELP: 'glossary',
GRAPHQL_EXPLORER: 'dev',
+ API_ACCESS: 'dev',
SETTINGS: 'table',
RATES_SUMMARY: 'summary',
RATE_EDIT: 'form',
diff --git a/services/app-web/src/gqlHelpers/apolloErrors.ts b/services/app-web/src/gqlHelpers/apolloErrors.ts
index 7e6a6db282..eb1fc3a65f 100644
--- a/services/app-web/src/gqlHelpers/apolloErrors.ts
+++ b/services/app-web/src/gqlHelpers/apolloErrors.ts
@@ -37,7 +37,7 @@ const handleNetworkError = (
}
const handleGQLErrors = (graphQLErrors: GraphQLErrors) => {
- graphQLErrors.forEach(({ message, locations, path, extensions }) => {
+ graphQLErrors.forEach(({ message, locations, path }) => {
recordJSException(
// Graphql errors mean something is wrong inside our api, maybe bad request or errors we return from api for known edge cases
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
diff --git a/services/app-web/src/index.tsx b/services/app-web/src/index.tsx
index c78bd155dc..33c55cd844 100644
--- a/services/app-web/src/index.tsx
+++ b/services/app-web/src/index.tsx
@@ -16,6 +16,11 @@ import type { S3BucketConfigType } from './s3/s3Amplify'
const gqlSchema = loader('../../app-web/src/gen/schema.graphql')
+const apiURL = process.env.REACT_APP_API_URL
+if (!apiURL || apiURL === '') {
+ throw new Error('REACT_APP_API_URL must be set to the url for the API')
+}
+
// We are using Amplify for communicating with Cognito, for now.
Amplify.configure({
Auth: {
@@ -44,7 +49,7 @@ Amplify.configure({
endpoints: [
{
name: 'api',
- endpoint: process.env.REACT_APP_API_URL,
+ endpoint: apiURL,
},
],
},
diff --git a/services/app-web/src/pages/APIAccess/APIAccess.module.scss b/services/app-web/src/pages/APIAccess/APIAccess.module.scss
new file mode 100644
index 0000000000..14a32be4ea
--- /dev/null
+++ b/services/app-web/src/pages/APIAccess/APIAccess.module.scss
@@ -0,0 +1,22 @@
+@use '../../styles/custom.scss' as custom;
+
+.pageContainer {
+ padding: 1em 5em;
+ max-width: custom.$mcr-container-standard-width-fixed;
+ background-color: custom.$mcr-foundation-white;
+}
+
+.centerButtonContainer {
+ display: flex;
+ margin: 1em;
+ justify-content: center;
+}
+
+.wrapKey {
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ background: custom.$mcr-gray-lighter;
+ display: block;
+ padding: 1em;
+ margin-top: 1em;
+}
diff --git a/services/app-web/src/pages/APIAccess/APIAccess.test.tsx b/services/app-web/src/pages/APIAccess/APIAccess.test.tsx
new file mode 100644
index 0000000000..2d68816800
--- /dev/null
+++ b/services/app-web/src/pages/APIAccess/APIAccess.test.tsx
@@ -0,0 +1,109 @@
+import { screen } from '@testing-library/react'
+import { Route, Routes } from 'react-router-dom'
+import { RoutesRecord } from '../../constants'
+import { renderWithProviders } from '../../testHelpers'
+import {
+ createAPIKeySuccess,
+ fetchCurrentUserMock,
+ mockValidCMSUser,
+ createAPIKeyNetworkError,
+} from '../../testHelpers/apolloMocks'
+import { APIAccess } from './APIAccess'
+
+describe('APIAccess', () => {
+ afterEach(() => {
+ jest.resetAllMocks()
+ })
+
+ it('renders without errors', async () => {
+ renderWithProviders(
+
+ } />
+ ,
+ {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidCMSUser(),
+ statusCode: 200,
+ }),
+ ],
+ },
+ routerProvider: {
+ route: '/dev/api-access',
+ },
+ }
+ )
+
+ const instructions = await screen.findByText(
+ 'To interact with the MC-Review API you will need a valid JWT'
+ )
+ expect(instructions).toBeInTheDocument()
+ })
+
+ it('displays an API key on success', async () => {
+ const { user } = renderWithProviders(
+
+ } />
+ ,
+ {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidCMSUser(),
+ statusCode: 200,
+ }),
+ createAPIKeySuccess(),
+ ],
+ },
+ routerProvider: {
+ route: '/dev/api-access',
+ },
+ }
+ )
+
+ const generateButton = await screen.findByRole('button', {
+ name: 'Generate API Key',
+ })
+ await user.click(generateButton)
+
+ const apiKey = await screen.findByRole('code', { name: 'API Key Text' })
+ const curlCmd = await screen.findByRole('code', {
+ name: 'Example Curl Command',
+ })
+
+ expect(apiKey).toBeInTheDocument()
+ expect(apiKey.textContent).toBe('foo.bar.baz.key123')
+ expect(curlCmd).toBeInTheDocument()
+ })
+
+ it('displays error on error', async () => {
+ const { user } = renderWithProviders(
+
+ } />
+ ,
+ {
+ apolloProvider: {
+ mocks: [
+ fetchCurrentUserMock({
+ user: mockValidCMSUser(),
+ statusCode: 200,
+ }),
+ createAPIKeyNetworkError(),
+ ],
+ },
+ routerProvider: {
+ route: '/dev/api-access',
+ },
+ }
+ )
+
+ const generateButton = await screen.findByRole('button', {
+ name: 'Generate API Key',
+ })
+ await user.click(generateButton)
+
+ const errorMsg = await screen.findByText('System error')
+ expect(errorMsg).toBeInTheDocument()
+ })
+})
diff --git a/services/app-web/src/pages/APIAccess/APIAccess.tsx b/services/app-web/src/pages/APIAccess/APIAccess.tsx
new file mode 100644
index 0000000000..da8dab7849
--- /dev/null
+++ b/services/app-web/src/pages/APIAccess/APIAccess.tsx
@@ -0,0 +1,155 @@
+import { ApolloError } from '@apollo/client'
+import { Button, Grid, GridContainer, Link } from '@trussworks/react-uswds'
+import path from 'path-browserify'
+import { useState } from 'react'
+import { RoutesRecord } from '../../constants'
+import {
+ CreateApiKeyPayload,
+ useCreateApiKeyMutation,
+} from '../../gen/gqlClient'
+import { handleApolloError } from '../../gqlHelpers/apolloErrors'
+import { recordJSException } from '../../otelHelpers'
+import { GenericErrorPage } from '../Errors/GenericErrorPage'
+import styles from './APIAccess.module.scss'
+
+function APIAccess(): React.ReactElement {
+ const apiURL = process.env.REACT_APP_API_URL
+
+ const thirdPartyAPIURL = !apiURL
+ ? undefined
+ : path.join(apiURL, '/v1/graphql/external')
+
+ const [getAPIKey] = useCreateApiKeyMutation()
+
+ const [displayErrorPage, setDisplayErrorPage] = useState(false)
+ const [apiKey, setAPIKey] = useState(
+ undefined
+ )
+
+ const callAPIKeyMutation = async () => {
+ try {
+ const result = await getAPIKey()
+
+ setAPIKey(result.data?.createAPIKey)
+ } catch (err) {
+ console.error('unexpected error generating a new API Key', err)
+ if (err instanceof ApolloError) {
+ handleApolloError(err, true)
+ }
+ recordJSException(err)
+ setDisplayErrorPage(true)
+ }
+ }
+
+ if (displayErrorPage) {
+ return
+ }
+
+ const copyKeyToClipboard = async () => {
+ if (apiKey) {
+ await navigator.clipboard.writeText(apiKey.key)
+ }
+ }
+
+ const curlCommand = !apiKey
+ ? undefined
+ : `
+curl -s ${thirdPartyAPIURL} -X POST \\
+-H "Authorization: Bearer ${apiKey.key}" \\
+-H "Content-Type: application/json" \\
+--data '{"query":"query IndexRates { indexRates { totalCount edges { node { id } } } }"}'
+`
+ return (
+
+
+
API Access
+
+
Credentials for using the MC-Review API
+
+ To interact with the MC-Review API you will need a valid JWT
+
+
+ {!apiKey ? (
+
+
+
+ ) : (
+ <>
+
+ {apiKey.key}
+
+
+
+
+
+ >
+ )}
+
+
Usage
+
+
Make GraphQL requests to the URL: {apiURL}
+
+ Include the JWT as a Bearer token in the Authorization
+ header. Example:
+