Skip to content

Commit

Permalink
Rebase statusing onto current main. (#11097)
Browse files Browse the repository at this point in the history
* Cypress fix2 (#11081)

* cypress fix and version bump

* updating audit version, disabling pa11y on local

* pre-commit action dislikes this.

* MDCT-2264: Create S3 Buckets + DynamoDB Table for MLR (#11079)

* adding MLR resources

* indentation and reference fix

* serverless fix

* how about I actually fix it

* duplicate removal

* naaar tables and form buckets (#11087)

* Clean up reporting APIs. (#11082)

* Add new error constants for Dynamo errors

Error constants

nerw constant

* Refactor reports/create.ts to handle errors more cleanly

Fix create route

* Refactor reports/archive.ts to handle errors more cleanly

Archive test should be calling `archive` route.

Previously, tests were calling `update` route.

* Refactor reports/fetch.ts to handle errors more cleanly

* Missing a required argument should return a `400` not a `500`.

Update test asserts to expect new status codes and error messages.

* Refactor reports/update.ts to handle errors more clearly

Reorganize code to move returned errors closer to their conditional checks. Adds additional error handling for S3 and Dynamo, and removes (likely) un-needed logic to archive reports from this route.

Remove duplicate `update` test.

* Archive route should ensure credentials

* Fetch handler was looking for wrong path parameter

* Add new discrete handler for archiving reports.

* Point archive button to the archive handler

Instead of the update handler

* Statusing completeness checker (#11095)

* shelving progress

* working on getting a basic test to work

* basic standard form validation

* adding fixtures

* more basic completion check

* shelving progress

* I want to keep record of this, but it will change.

* mid-refactor, but time for guitar lessons

* one last quick checkin

* I've got 99 problems and all of them are async

* commenting out broken tests, fixing async

* started creation of required schema

* updating fixture to fail, removing review/submit

* new form test

* new completion function

* now with modals

* Some changes and empty plan fixture

* params for fixture testing

* resolved test, renamed functions

* extracting completion check into it's own file

* extracted code into seperate file

* adding conditional calculation to fetch

* resolving issue with new reports

* cleaning up testing fixtures

* adding comments

---------

Co-authored-by: Thomas Blackwell <thomas.blackwell@coforma.io>
Co-authored-by: Karla Valcárcel Martínez <99458559+karla-vm@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 28, 2023
1 parent 3973e98 commit ccc2b57
Show file tree
Hide file tree
Showing 27 changed files with 766 additions and 500 deletions.
6 changes: 5 additions & 1 deletion .env_example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ DYNAMODB_URL=http://localhost:8000
LOCAL_LOGIN=true
MCPAR_FORM_BUCKET=local-mcpar-form
MCPAR_REPORT_TABLE_NAME=local-mcpar-reports
MLR_FORM_BUCKET=local-mlr-form
MLR_REPORT_TABLE_NAME=local-mlr-reports
NAAAR_REPORT_TABLE_NAME=local-naaar-reports
NAAAR_FORM_BUCKET=local-naaar-form
PRINCE_API_HOST=macpro-platform-dev.cms.gov
PRINCE_API_PATH=/doc-conv/508html-to-508pdf
S3_ATTACHMENTS_BUCKET_NAME=local-uploads
Expand All @@ -20,4 +24,4 @@ IAM_PERMISSIONS_BOUNDARY="bound"
WARMUP_SCHEDULE=60 minutes
WARMUP_CONCURRENCY=5
DATATRANSFORM_ENABLED=false
DATATRANSFORM_UPDATED_ENABLED=false
DATATRANSFORM_UPDATED_ENABLED=false
1 change: 1 addition & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ jobs:
CYPRESS_STATE_USER_PASSWORD: ${{ secrets.CYPRESS_STATE_USER_PASSWORD }}
CYPRESS_ADMIN_USER_EMAIL: ${{ secrets.CYPRESS_ADMIN_USER_EMAIL }}
CYPRESS_ADMIN_USER_PASSWORD: ${{ secrets.CYPRESS_ADMIN_USER_PASSWORD }}
RUN_PA11Y: true
- name: Upload screenshots
uses: actions/upload-artifact@v2
if: failure()
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ DYNAMODB_URL=http://localhost:8000
LOCAL_LOGIN=true
MCPAR_FORM_BUCKET=local-mcpar-form
MCPAR_REPORT_TABLE_NAME=local-mcpar-reports
MLR_FORM_BUCKET=local-mlr-form
MLR_REPORT_TABLE_NAME=local-mlr-reports
NAAAR_FORM_BUCKET=local-naaar-form
NAAAR_REPORT_TABLE_NAME=local-naaar-reports
PRINCE_API_HOST=macpro-platform-dev.cms.gov
PRINCE_API_PATH=/doc-conv/508html-to-508pdf
S3_ATTACHMENTS_BUCKET_NAME=local-uploads
Expand Down Expand Up @@ -209,8 +213,8 @@ While not necessary, it might be beneficial to have AWS CLI installed/configured

### Deployment Steps

| branch | status | release |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| branch | status | release |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Main | [![Deploy](https://github.com/Enterprise-CMCS/macpro-mdct-mcr/actions/workflows/deploy.yml/badge.svg?branch=main)](https://github.com/Enterprise-CMCS/macpro-mdct-mcr/actions/workflows/deploy.yml) | [![release to main](https://img.shields.io/badge/-Create%20PR-blue.svg)](https://github.com/Enterprise-CMCS/macpro-mdct-mcr/compare?quick_pull=1) |
| Val | [![Deploy](https://github.com/Enterprise-CMCS/macpro-mdct-mcr/actions/workflows/deploy.yml/badge.svg?branch=val)](https://github.com/Enterprise-CMCS/macpro-mdct-mcr/actions/workflows/deploy.yml) | [![release to val](https://img.shields.io/badge/-Create%20PR-blue.svg)](https://github.com/Enterprise-CMCS/macpro-mdct-mcr/compare/val...main?quick_pull=1) |
| Production | [![Deploy](https://github.com/Enterprise-CMCS/macpro-mdct-mcr/actions/workflows/deploy.yml/badge.svg?branch=production)](https://github.com/Enterprise-CMCS/macpro-mdct-mcr/actions/workflows/deploy.yml) | [![release to production](https://img.shields.io/badge/-Create%20PR-blue.svg)](https://github.com/Enterprise-CMCS/macpro-mdct-mcr/compare/production...val?quick_pull=1) |
Expand Down Expand Up @@ -241,12 +245,12 @@ If you have a PR that needs Product/Design input, the easiest way to get it to t

![Architecture Diagram](./.images/architecture.svg?raw=true)


**General Structure** - React frontend that renders a form for a user to fill out, Node backend that uses S3 and Dynamo to store and validate forms.

**Custom JSON & form field creation engine (formFieldFactory)** - Each report has a custom JSON object, stored in a JSON file, written using a custom schema. This JSON object is referred to as the form template and it is the blueprint from which report form fields are created. It is also used to create routes and navigation elements throughout the app. When provided form fields from this template, the formFieldFactory renders the appropriate form fields. A similar process occurs when a report is exported in PDF preview format.

**Page and Form Structure** Each page has a name, path, and pageType, for example the first page a user sees in the form will be have ‘pageType: standard’ with a ‘verbiage’ object that includes all of the text that precedes the form fields. The the ‘form’ object follows with a unique id and ‘fields’ array that holds one or more objects that represent the individual questions in a form. There are different types of forms as well. If there is a "pageType": "modalDrawer", then instead of a ‘form’ object, it will have a ‘modalForm’ object. Here is an example of a standard page with one field:

```json
{
"name": "Standard Page",
Expand Down Expand Up @@ -307,7 +311,7 @@ Dropdown and dynamic fields are not currently supported as nested child fields.

**Choice ids** Fields which accept a list of choices (radio, checkbox, dropdown) require choices with unique, immutable ids. These ids must remain immutable even across versions of the form template to ensure they can be relied on and referenced by downstream data analysts. We have chosen to manually generate these ids.

**Nuanced behaviors like the “-otherText”** flag on a question’s id Most of the structure of the form template schema is captured in the types contained in types/index.tsx however there are some behaviors like the otherText flag that are not. For example, when a report is exported to PDF, subquestions like nested optional text area fields for the purpose of providing additional information must have ids that end in -otherText or they will not render the entered answer correctly.
**Nuanced behaviors like the “-otherText”** flag on a question’s id Most of the structure of the form template schema is captured in the types contained in types/index.tsx however there are some behaviors like the otherText flag that are not. For example, when a report is exported to PDF, subquestions like nested optional text area fields for the purpose of providing additional information must have ids that end in -otherText or they will not render the entered answer correctly.

**Form** We use react-hook-form for form state management. The formFieldFactory renders individual field inputs and registers them with RHF which exposes an onSubmit callback hook that is used to check error states and display inline validation messaging.

Expand All @@ -320,6 +324,7 @@ Dropdown and dynamic fields are not currently supported as nested child fields.
**CustomHTML parser** - function checks if element is a string, if so then the element will be passed in the function “sanitize” from "dompurify", and then the result from that process gets passed into the function “parse” from "html-react-parser" and the result gets returned. If the element is not a string, then the elements are treated as an array and get mapped over returning a key, as, and spread the props. The last check is in this else block, checking whether the element is ‘html’, in which case the content will get passed through ‘sanitize’ and ‘parse’ and the ‘as’ prop gets deleted before returning the modified element type, element props, and content.

**Dynamo macpar-reports vs macpar-form in S3 Storage** - When a user creates a form, it is stored in Dynamo and tracks user information such as when the program was last edited and by whom, date submitted’ report period start and end date, program name, report type, the state, id, and status. The file in the S3 bucket is the entire form of user inputted data, and this is a pattern that is unique to this project. S3 is mainly used for attachments, data for virus scans on attachments, mathematica integration. We decided to store the programs in S3 because these data can get so large that we can’t reliably store it all in Dynamo, nor search through them without the app breaking.

## Copyright and license

[![License](https://img.shields.io/badge/License-CC0--1.0--Universal-blue.svg)](https://creativecommons.org/publicdomain/zero/1.0/legalcode)
Expand Down
27 changes: 21 additions & 6 deletions services/app-api/handlers/reports/archive.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { fetchReport } from "./fetch";
import { updateReport } from "./update";
import { APIGatewayProxyEvent } from "aws-lambda";
import { proxyEvent } from "../../utils/testing/proxyEvent";
import { StatusCodes } from "../../utils/types/types";
import { mockReport } from "../../utils/testing/setupJest";
import { error } from "../../utils/constants/constants";
import { archiveReport } from "./archive";

jest.mock("../../utils/auth/authorization", () => ({
isAuthorized: jest.fn().mockResolvedValue(true),
hasPermissions: jest.fn(() => {}),
}));

const mockAuthUtil = require("../../utils/auth/authorization");

jest.mock("../../utils/debugging/debug-lib", () => ({
Expand Down Expand Up @@ -41,8 +42,9 @@ describe("Test archiveReport method", () => {
beforeEach(() => {
// fail state and pass admin auth checks
mockAuthUtil.hasPermissions
.mockReturnValueOnce(false)
.mockReturnValueOnce(true);
.mockReturnValueOnce(true)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false);
});
afterEach(() => {
jest.clearAllMocks();
Expand All @@ -57,8 +59,7 @@ describe("Test archiveReport method", () => {
},
body: JSON.stringify(mockReport),
});
const res: any = await updateReport(archiveEvent, null);

const res: any = await archiveReport(archiveEvent, null);
const body = JSON.parse(res.body);
expect(res.statusCode).toBe(StatusCodes.SUCCESS);
expect(body.archived).toBe(true);
Expand All @@ -73,8 +74,22 @@ describe("Test archiveReport method", () => {
},
body: undefined!,
});
const res = await updateReport(archiveEvent, null);
const res = await archiveReport(archiveEvent, null);
expect(res.statusCode).toBe(StatusCodes.NOT_FOUND);
expect(res.body).toContain(error.NO_MATCHING_RECORD);
});

test("Test archive report without admin permissions throws 403", async () => {
mockedFetchReport.mockResolvedValue({
statusCode: 200,
headers: {
"Access-Control-Allow-Origin": "string",
"Access-Control-Allow-Credentials": true,
},
body: undefined!,
});
const res = await archiveReport(archiveEvent, null);
expect(res.statusCode).toBe(StatusCodes.UNAUTHORIZED);
expect(res.body).toContain(error.UNAUTHORIZED);
});
});
71 changes: 44 additions & 27 deletions services/app-api/handlers/reports/archive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,57 @@ import handler from "../handler-lib";
import { fetchReport } from "./fetch";
// utils
import dynamoDb from "../../utils/dynamo/dynamodb-lib";
import { StatusCodes } from "../../utils/types/types";
import { StatusCodes, UserRoles } from "../../utils/types/types";
import { error } from "../../utils/constants/constants";
import { hasPermissions } from "../../utils/auth/authorization";

export const archiveReport = handler(async (event, context) => {
let status, body;
// get current report
// Return a 403 status if the user is not an admin.
if (!hasPermissions(event, [UserRoles.ADMIN])) {
return {
status: StatusCodes.UNAUTHORIZED,
body: error.UNAUTHORIZED,
};
}

// Get current report
const reportEvent = { ...event, body: "" };
const getCurrentReport = await fetchReport(reportEvent, context);

// if current report exists, parse for archived status
if (getCurrentReport?.body) {
const currentReport = JSON.parse(getCurrentReport.body);
const currentArchivedStatus = currentReport?.archived;

// Delete raw data prior to updating
delete currentReport.fieldData;
delete currentReport.formTemplate;

// toggle archived status in report metadata table
const reportMetadataParams = {
TableName: process.env.MCPAR_REPORT_TABLE_NAME!,
Item: {
...currentReport,
archived: !currentArchivedStatus,
},
if (!getCurrentReport.body) {
return {
status: StatusCodes.NOT_FOUND,
body: error.NO_MATCHING_RECORD,
};
await dynamoDb.put(reportMetadataParams);
}

const currentReport = JSON.parse(getCurrentReport.body);
const currentArchivedStatus = currentReport?.archived;

// set response status and body
status = StatusCodes.SUCCESS;
body = reportMetadataParams.Item;
} else {
status = StatusCodes.NOT_FOUND;
body = error.NO_MATCHING_RECORD;
// Delete old data prior to updating
delete currentReport.fieldData;
delete currentReport.formTemplate;

// Toggle archived state in DynamoDB.
const reportMetadataParams = {
TableName: process.env.MCPAR_REPORT_TABLE_NAME!,
Item: {
...currentReport,
archived: !currentArchivedStatus,
},
};

try {
await dynamoDb.put(reportMetadataParams);
} catch (err) {
return {
status: StatusCodes.SERVER_ERROR,
body: error.DYNAMO_UPDATE_ERROR,
};
}
return { status, body };

return {
status: StatusCodes.SUCCESS,
body: reportMetadataParams.Item,
};
});
8 changes: 4 additions & 4 deletions services/app-api/handlers/reports/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,25 +68,25 @@ describe("Test createReport API method", () => {
expect(res.body).toContain(error.MISSING_DATA);
});

test("Test reportKey not provided throws 500 error", async () => {
test("Test reportKey not provided throws 400 error", async () => {
const noKeyEvent: APIGatewayProxyEvent = {
...creationEvent,
pathParameters: {},
};
const res = await createReport(noKeyEvent, null);

expect(res.statusCode).toBe(StatusCodes.SERVER_ERROR);
expect(res.statusCode).toBe(StatusCodes.BAD_REQUEST);
expect(res.body).toContain(error.NO_KEY);
});

test("Test reportKey empty throws 500 error", async () => {
test("Test reportKey empty throws 400 error", async () => {
const noKeyEvent: APIGatewayProxyEvent = {
...creationEvent,
pathParameters: { state: "" },
};
const res = await createReport(noKeyEvent, null);

expect(res.statusCode).toBe(StatusCodes.SERVER_ERROR);
expect(res.statusCode).toBe(StatusCodes.BAD_REQUEST);
expect(res.body).toContain(error.NO_KEY);
});
});
Loading

0 comments on commit ccc2b57

Please sign in to comment.