Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PIMS-1291 React Error Boundary #2193

Merged
merged 21 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions express-api/src/controllers/reports/errorReportSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import z from 'zod';

/**
* @description Zod schema for validating frontend error reports.
*/
export const errorReportSchema = z.object({
// Only specified the fields we're using.
user: z.object({
client_roles: z.array(z.string()).optional(),
email: z.string().email(),
preferred_username: z.string(),
display_name: z.string(),
}),
userMessage: z.string(),
error: z.object({
message: z.string(),
stack: z.string(),
}),
timestamp: z.string(),
});

/**
* @description Type inferred from Zod schema errorReportSchema
* @type ErrorReport
*/
export type ErrorReport = z.infer<typeof errorReportSchema>;
21 changes: 21 additions & 0 deletions express-api/src/controllers/reports/reportsController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logger from '@/utilities/winstonLogger';
import { stubResponse } from '../../utilities/stubResponse';
import { Request, Response } from 'express';
import { ErrorReport, errorReportSchema } from '@/controllers/reports/errorReportSchema';

/**
* @description Get all reports as a CSV or Excel file.
Expand Down Expand Up @@ -51,3 +53,22 @@ export const getSpreadsheetUsersReports = async (req: Request, res: Response) =>
*/
return stubResponse(res);
};

export const submitErrorReport = async (req: Request, res: Response) => {
/**
* #swagger.tags = ['Reports']
* #swagger.description = 'Accepts an error report from the frontend and sends an email to administrators.'
* #swagger.security = [{
* "bearerAuth" : []
* }]
*/
const info: ErrorReport = req.body;
logger.info(info);
try {
errorReportSchema.parse(info);
} catch (e) {
return res.status(400).send(e);
}
// TODO: Add email component after CHES is in. Response depends on that outcome.
return res.status(200).send(info);
};
3 changes: 3 additions & 0 deletions express-api/src/routes/reportsRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ router.route('/users').get(controllers.getSpreadsheetUsersReports);
* ie. /reports/properties?include=agency,classification,type
*/

// For errors submitted by the frontend Error Boundary.
router.route('/error').post(controllers.submitErrorReport);

export default router;
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {
getSpreadsheetProjectsReports,
getSpreadsheetPropertiesReports,
getSpreadsheetUsersReports,
submitErrorReport,
} from '@/controllers/reports/reportsController';
import { ErrorReport } from '@/controllers/reports/errorReportSchema';
import { Roles } from '@/constants/roles';
import logger from '@/utilities/winstonLogger';

describe('UNIT - Reports', () => {
let mockRequest: Request & MockReq, mockResponse: Response & MockRes;
Expand Down Expand Up @@ -86,4 +90,39 @@ describe('UNIT - Reports', () => {
expect(mockResponse.statusValue).toBe(200);
});
});

// TODO: Add additional test cases when this endpoint starts using CHES
describe('POST /reports/error', () => {
const error: ErrorReport = {
user: {
client_roles: [Roles.ADMIN],
preferred_username: '123@idir',
display_name: 'Tester',
email: 'tester@gov.bc.ca',
},
userMessage: 'Help, it broke.',
timestamp: new Date().toLocaleString(),
error: new Error('Bad Error'),
};

afterEach(() => {
jest.clearAllMocks();
});
it('should return 200 status and error info in body', async () => {
mockRequest.body = error;
const mockLogger = jest.spyOn(logger, 'info');
await submitErrorReport(mockRequest, mockResponse);
expect(mockLogger).toHaveBeenCalledTimes(1);
expect(mockResponse.statusValue).toBe(200);
expect(mockResponse.sendValue).toBe(error);
});

it('should return 400 status when body does not match zod schema', async () => {
mockRequest.body = {
fakeFields: 'Should fail check',
};
await submitErrorReport(mockRequest, mockResponse);
expect(mockResponse.statusValue).toBe(400);
});
});
});
1 change: 1 addition & 0 deletions react-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"node-xlsx": "0.23.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "4.0.12",
"react-hook-form": "7.50.1",
"react-router-dom": "6.22.0"
},
Expand Down
98 changes: 51 additions & 47 deletions react-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,62 +11,66 @@ import AuthRouteGuard from './guards/AuthRouteGuard';
import BaseLayout from './components/layout/BaseLayout';
import { AccessRequest } from './pages/AccessRequest';
import UsersManagement from './pages/UsersManagement';
import { ErrorBoundary } from 'react-error-boundary';
import ErrorFallback from '@/pages/ErrorFallback';

const Router = () => {
return (
<ConfigContextProvider>
<AuthContextProvider>
<Routes>
<Route
index
element={
<BaseLayout displayFooter>
<Home />
</BaseLayout>
}
/>
<Route
path="/access-request"
element={
<BaseLayout displayFooter>
<AuthRouteGuard>
<AccessRequest />
</AuthRouteGuard>
</BaseLayout>
}
/>
<Route
path="/dev"
element={
<BaseLayout>
<AuthRouteGuard>
<Dev />
</AuthRouteGuard>
</BaseLayout>
}
/>
<Route path="/admin">
<Route
path="users"
element={
<BaseLayout>
<AuthRouteGuard>
<UsersManagement />
</AuthRouteGuard>
</BaseLayout>
}
/>
</Route>
</Routes>
</AuthContextProvider>
</ConfigContextProvider>
<Routes>
<Route
index
element={
<BaseLayout displayFooter>
<Home />
</BaseLayout>
}
/>
<Route
path="/access-request"
element={
<BaseLayout displayFooter>
<AuthRouteGuard>
<AccessRequest />
</AuthRouteGuard>
</BaseLayout>
}
/>
<Route
path="/dev"
element={
<BaseLayout>
<AuthRouteGuard>
<Dev />
</AuthRouteGuard>
</BaseLayout>
}
/>
<Route path="/admin">
<Route
path="users"
element={
<BaseLayout>
<AuthRouteGuard>
<UsersManagement />
</AuthRouteGuard>
</BaseLayout>
}
/>
</Route>
</Routes>
);
};

const App = () => {
return (
<ThemeProvider theme={appTheme}>
<Router />
<ErrorBoundary FallbackComponent={ErrorFallback}>
<ConfigContextProvider>
<AuthContextProvider>
<Router />
</AuthContextProvider>
</ConfigContextProvider>
</ErrorBoundary>
</ThemeProvider>
);
};
Expand Down
Loading
Loading