From b8997e4f89a479beef1dc61c810bbdcfd4e30ea6 Mon Sep 17 00:00:00 2001 From: Marc Wodahl Date: Mon, 16 Sep 2024 13:20:46 -0600 Subject: [PATCH 01/11] Add RSU Errors page --- webapp/src/EnvironmentVars.tsx | 1 + webapp/src/apis/rsu-api.ts | 9 + webapp/src/components/AdminTable.tsx | 6 +- webapp/src/components/RsuErrorSummary.tsx | 132 +++++++++ webapp/src/features/menu/DisplayRsuErrors.tsx | 266 ++++++++++++++++++ webapp/src/features/menu/Menu.css | 19 ++ webapp/src/features/menu/Menu.tsx | 26 +- webapp/src/features/menu/menuSlice.tsx | 7 +- webapp/src/pages/Map.tsx | 6 +- webapp/src/pages/mapSlice.tsx | 25 ++ webapp/src/store.tsx | 2 + 11 files changed, 490 insertions(+), 9 deletions(-) create mode 100644 webapp/src/components/RsuErrorSummary.tsx create mode 100644 webapp/src/features/menu/DisplayRsuErrors.tsx create mode 100644 webapp/src/pages/mapSlice.tsx diff --git a/webapp/src/EnvironmentVars.tsx b/webapp/src/EnvironmentVars.tsx index ba6f7b2e..f40e929b 100644 --- a/webapp/src/EnvironmentVars.tsx +++ b/webapp/src/EnvironmentVars.tsx @@ -63,6 +63,7 @@ class EnvironmentVars { static adminAddOrg = `${this.getBaseApiUrl()}/admin-new-org` static adminOrg = `${this.getBaseApiUrl()}/admin-org` static contactSupport = `${this.getBaseApiUrl()}/contact-support` + static rsuErrorSummary = `${this.getBaseApiUrl()}/rsu-error-summary` } export default EnvironmentVars diff --git a/webapp/src/apis/rsu-api.ts b/webapp/src/apis/rsu-api.ts index 231924f7..f2951fb0 100644 --- a/webapp/src/apis/rsu-api.ts +++ b/webapp/src/apis/rsu-api.ts @@ -171,6 +171,15 @@ class RsuApi { body: JSON.stringify(json), }) } + + // POST + postRsuErrorSummary = async (json: Object): Promise> => { + console.log('api: ', json) + return await apiHelper._postData({ + url: EnvironmentVars.rsuErrorSummary, + body: JSON.stringify(json), + }) + } } const rsuApiObject = new RsuApi() diff --git a/webapp/src/components/AdminTable.tsx b/webapp/src/components/AdminTable.tsx index 36e09829..25958ad0 100644 --- a/webapp/src/components/AdminTable.tsx +++ b/webapp/src/components/AdminTable.tsx @@ -11,6 +11,8 @@ interface AdminTableProps { data: any[] title: string editable?: any + selection?: boolean + tableLayout?: 'auto' | 'fixed' } const AdminTable = (props: AdminTableProps) => { @@ -25,9 +27,9 @@ const AdminTable = (props: AdminTableProps) => { title={props.title} editable={props.editable} options={{ - selection: true, + selection: props.selection === undefined ? true : props.selection, actionsColumnIndex: -1, - tableLayout: 'fixed', + tableLayout: props.tableLayout === undefined ? 'fixed' : props.tableLayout, rowStyle: { overflowWrap: 'break-word', }, diff --git a/webapp/src/components/RsuErrorSummary.tsx b/webapp/src/components/RsuErrorSummary.tsx new file mode 100644 index 00000000..dc365689 --- /dev/null +++ b/webapp/src/components/RsuErrorSummary.tsx @@ -0,0 +1,132 @@ +import React, { useState } from 'react' +import { Form } from 'react-bootstrap' +import { useForm } from 'react-hook-form' + +import 'react-widgets/styles.css' +import RsuApi from '../apis/rsu-api' + +import './css/ContactSupportMenu.css' +import toast from 'react-hot-toast' +import Dialog from '@mui/material/Dialog' +import { DialogActions, DialogContent, DialogTitle } from '@mui/material' +import { RsuOnlineStatus } from '../apis/rsu-api-types' + +type RsuErrorSummaryType = { + rsu: 'string' + online_status: RsuOnlineStatus | string + scms_status: string + hidden: boolean + setHidden: () => void +} + +const RsuErrorSummary = (props: RsuErrorSummaryType) => { + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm() + + const onSubmit = async (data: Object) => { + try { + const res = await RsuApi.postRsuErrorSummary(data) + const status = res.status + if (status === 200) { + toast.success('Successfully sent RSU summary email') + reset() + } else { + toast.error('Something went wrong: ' + status) + } + } catch (exception_var) { + toast.error('An exception occurred, please try again later') + } + props.setHidden() + } + + const messageTable = ` + + + + + + + + + +
Online StatusSCMS Status
${'RSU ' + props.online_status}${props.scms_status}
+` + + const message = ` +

RSU Error Summary Email

+
+

Hello,

+

Below is the error summary for RSU ${props.rsu} at ${new Date().toISOString()} UTC:

+ ${messageTable} +` + + return ( + + RSU Error Summary Email + +
+ + Send To + + {errors.email && {errors.email.message}} + + + Subject + + {errors.subject && {errors.subject.message}} + + + Message + + {errors.message && {errors.message.message}} + +
+
+ + + + +
+ ) +} + +export default RsuErrorSummary diff --git a/webapp/src/features/menu/DisplayRsuErrors.tsx b/webapp/src/features/menu/DisplayRsuErrors.tsx new file mode 100644 index 00000000..3b280d45 --- /dev/null +++ b/webapp/src/features/menu/DisplayRsuErrors.tsx @@ -0,0 +1,266 @@ +import React, { useEffect, useState } from 'react' +import { useSelector, useDispatch } from 'react-redux' + +import { getIssScmsStatus, selectRsuData } from '../../generalSlices/rsuSlice' + +import '../../components/css/SnmpwalkMenu.css' +import { theme } from '../../styles' +import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' +import { Action } from '@material-table/core' +import { RootState } from '../../store' +import { selectRsuOnlineStatus, selectIssScmsStatusData } from '../../generalSlices/rsuSlice' + +import { PlaceOutlined, ArrowBackIos } from '@mui/icons-material' +import ExpandMoreIcon from '@mui/icons-material/ExpandMore' + +import AdminTable from '../../components/AdminTable' +import { setMapViewState } from '../../pages/mapSlice' +import { + Accordion, + AccordionDetails, + AccordionSummary, + Button, + StyledEngineProvider, + ThemeProvider, + Typography, +} from '@mui/material' +import RsuErrorSummary from '../../components/RsuErrorSummary' + +const DisplayRsuErrors = () => { + const dispatch: ThunkDispatch = useDispatch() + const rsuData = useSelector(selectRsuData) + const rsuOnlineStatus = useSelector(selectRsuOnlineStatus) + const issScmsStatusData = useSelector(selectIssScmsStatusData) + const [selectedRSU, setSelectedRSU] = useState(null) + const [emailHidden, setEmailHidden] = useState(true) + + type RsuErrorRowType = { + rsu: string + online_status: string + lat: number + lon: number + scms_status: string + } + + // UseEffect to pull SCMS status data on first load + useEffect(() => { + dispatch(getIssScmsStatus()) + }, []) + + const getRSUOnlineStatus = (rsuIpv4: string) => { + return rsuIpv4 in rsuOnlineStatus && rsuOnlineStatus[rsuIpv4].hasOwnProperty('current_status') + ? rsuOnlineStatus[rsuIpv4].current_status + : 'Offline' + } + + const getRSULastOnline = (rsuIpv4: string): string => { + return rsuIpv4 in rsuOnlineStatus && rsuOnlineStatus[rsuIpv4].hasOwnProperty('last_online') + ? rsuOnlineStatus[rsuIpv4].last_online + : 'No Data' + } + + const getRSUSCMSStatus = (rsuIpv4: string) => { + return issScmsStatusData.hasOwnProperty(rsuIpv4) && issScmsStatusData[rsuIpv4] + ? issScmsStatusData[rsuIpv4].health + : '0' + } + + const getRSUSCMSExpiration = (rsuIpv4: string) => { + return issScmsStatusData.hasOwnProperty(rsuIpv4) && + issScmsStatusData[rsuIpv4] !== null && + issScmsStatusData[rsuIpv4].hasOwnProperty('expiration') + ? issScmsStatusData[rsuIpv4].expiration + : 'Never downloaded certificates' + } + + const getRSUSCMSDisplay = (rsuIpv4: string) => { + if (getRSUSCMSStatus(rsuIpv4) === '0') { + var rsu_scms_status = 'SCMS Unhealthy' + let rsu_scms_expiration = getRSUSCMSExpiration(rsuIpv4) + switch (rsu_scms_expiration) { + case 'Never downloaded certificates': + rsu_scms_status += ' (RSU Never downloaded certificates)' + break + default: + try { + let expiration_date = new Date(rsu_scms_expiration) + let now = new Date() + let diff = expiration_date.getTime() - now.getTime() + if (diff < 0) { + rsu_scms_status += ' (RSU SCMS certificate expired)' + } + } catch (e) { + console.debug('Error parsing SCMS expiration date: ', e) + } + break + } + } else { + rsu_scms_status = 'SCMS Healthy' + } + return rsu_scms_status + } + + // Create RSU Errors Table Data + const rsuTableData = rsuData.map((rsu) => { + var rsu_online_status = 'RSU ' + getRSUOnlineStatus(rsu.properties.ipv4_address) + + var rsu_scms_status = getRSUSCMSDisplay(rsu.properties.ipv4_address) + + return { + rsu: rsu.properties.ipv4_address, + road: rsu.properties.primary_route, + lat: rsu.geometry.coordinates[1], + lon: rsu.geometry.coordinates[0], + online_status: rsu_online_status, + scms_status: rsu_scms_status, + } + }) + + const tableActions: Action[] = [ + { + icon: () => , + tooltip: 'View RSU on Map', + position: 'row', + onClick: (event, rowData: RsuErrorRowType) => { + dispatch(setMapViewState({ latitude: rowData.lat, longitude: rowData.lon, zoom: 15 })) + setSelectedRSU(rsuData.find((rsu) => rsu.properties.ipv4_address === rowData.rsu)) + }, + }, + ] + + const setHidden = () => { + console.log('setHidden: ', !emailHidden) + setEmailHidden(!emailHidden) + } + + const containerStyle = { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + margin: 'auto', + TextAlign: 'center', + FlexDirection: 'column', + } + + const errorPageStyle = { + backgroundColor: 'rgb(14, 32, 82)', + borderTop: '1px solid white', + borderBottom: '1px solid white', + fontFamily: 'Arial Helvetica Sans-Serif', + width: '90%', + padding: '0.5rem 1rem', + } + + return ( +
+ {selectedRSU !== null ? ( +
+

+ {selectedRSU.properties.ipv4_address} Errors +

+ { + setSelectedRSU(null) + }} + /> +
+ + + + }> + Online Status + + +
+

+ RSU Online Status: + {getRSUOnlineStatus(selectedRSU.properties.ipv4_address)} +

+
+

+ RSU Last Online: + {getRSULastOnline(selectedRSU.properties.ipv4_address)} +

+
+
+
+ + }> + SCMS Status + + +
+

+ SCMS Status: + {getRSUSCMSStatus(selectedRSU.properties.ipv4_address) === '1' ? 'Healthy' : 'Unhealthy'} +

+
+

+ SCMS Expiration: + {getRSUSCMSExpiration(selectedRSU.properties.ipv4_address)} +

+
+
+
+
+
+
+
+ +
+
+ ) : ( +
+

+ RSU Errors +

+ +
+ )} +
+ ) +} + +export default DisplayRsuErrors diff --git a/webapp/src/features/menu/Menu.css b/webapp/src/features/menu/Menu.css index 109ecbd2..3cdb57d1 100644 --- a/webapp/src/features/menu/Menu.css +++ b/webapp/src/features/menu/Menu.css @@ -79,6 +79,25 @@ background-color: #b55e12; color: white; } +#rsu-errors-toggle { + height: 35px; + border: none; + padding: 1px 12px; + text-align: center; + text-decoration: none; + font-size: 16px; + margin: 4px 2px; + cursor: pointer; + border-radius: 30px; + margin-left: 100px; + position: absolute; + z-index: 100; + margin-top: 10px; + right: 10px; + top: 185px; + background-color: #b55e12; + color: white; +} #toggle_config { height: 35px; border: none; diff --git a/webapp/src/features/menu/Menu.tsx b/webapp/src/features/menu/Menu.tsx index 8e6a1339..e6ef286c 100644 --- a/webapp/src/features/menu/Menu.tsx +++ b/webapp/src/features/menu/Menu.tsx @@ -5,9 +5,10 @@ import { useSelector, useDispatch } from 'react-redux' import { selectRole } from '../../generalSlices/userSlice' import { selectCountList, selectSelectedRsu } from '../../generalSlices/rsuSlice' import { selectConfigList } from '../../generalSlices/configSlice' -import { selectDisplayCounts, selectView, setDisplay, setSortedCountList } from './menuSlice' +import { selectDisplayCounts, selectView, setDisplay, setSortedCountList, selectDisplayRsuErrors } from './menuSlice' import { SecureStorageManager } from '../../managers' import DisplayCounts from './DisplayCounts' +import DisplayRsuErrors from './DisplayRsuErrors' import ConfigureRSU from './ConfigureRSU' import { AnyAction, ThunkDispatch } from '@reduxjs/toolkit' import { RootState } from '../../store' @@ -31,6 +32,7 @@ const Menu = () => { const selectedRsu = useSelector(selectSelectedRsu) const selectedRsuList = useSelector(selectConfigList) const displayCounts = useSelector(selectDisplayCounts) + const displayRsuErrors = useSelector(selectDisplayRsuErrors) const view = useSelector(selectView) useEffect(() => { @@ -41,19 +43,37 @@ const Menu = () => {
{view === 'buttons' && !selectedRsu && selectedRsuList?.length === 0 && (
-
)} + {view === 'buttons' && !selectedRsu && selectedRsuList?.length === 0 && ( +
+ +
+ )} {view === 'tab' && displayCounts === true && !selectedRsu && selectedRsuList?.length === 0 && (
-
)} + {view === 'tab' && displayRsuErrors === true && !selectedRsu && selectedRsuList?.length === 0 && ( +
+ + +
+ )} {SecureStorageManager.getUserRole() === 'admin' && (selectedRsu || selectedRsuList?.length > 0) && (
diff --git a/webapp/src/features/menu/menuSlice.tsx b/webapp/src/features/menu/menuSlice.tsx index 6cd744a9..45bb854d 100644 --- a/webapp/src/features/menu/menuSlice.tsx +++ b/webapp/src/features/menu/menuSlice.tsx @@ -8,6 +8,7 @@ const initialState = { currentSort: null as null | string, sortedCountList: [] as CountsListElement[], displayCounts: false, + displayRsuErrors: false, view: 'buttons', } @@ -74,8 +75,9 @@ export const menuSlice = createSlice({ state.value.sortedCountList = action.payload }, setDisplay: (state, action) => { - state.value.view = action.payload - state.value.displayCounts = action.payload == 'tab' + state.value.view = action.payload.view + state.value.displayCounts = action.payload.display == 'displayCounts' + state.value.displayRsuErrors = action.payload.display == 'displayRsuErrors' }, }, }) @@ -86,6 +88,7 @@ export const selectLoading = (state: RootState) => state.menu.loading export const selectCurrentSort = (state: RootState) => state.menu.value.currentSort export const selectSortedCountList = (state: RootState) => state.menu.value.sortedCountList export const selectDisplayCounts = (state: RootState) => state.menu.value.displayCounts +export const selectDisplayRsuErrors = (state: RootState) => state.menu.value.displayRsuErrors export const selectView = (state: RootState) => state.menu.value.view export default menuSlice.reducer diff --git a/webapp/src/pages/Map.tsx b/webapp/src/pages/Map.tsx index 0b470281..ca4fa62f 100644 --- a/webapp/src/pages/Map.tsx +++ b/webapp/src/pages/Map.tsx @@ -89,6 +89,7 @@ import { setSelectedIntersectionId, } from '../generalSlices/intersectionSlice' import { mapTheme } from '../styles' +import { selectViewState, setMapViewState } from './mapSlice' // @ts-ignore: workerClass does not exist in typed mapboxgl // eslint-disable-next-line import/no-webpack-loader-syntax @@ -134,7 +135,8 @@ function MapPage(props: MapPageProps) { const selectedIntersection = useSelector(selectSelectedIntersection) // Mapbox local state variables - const [viewState, setViewState] = useState(EnvironmentVars.getMapboxInitViewState()) + + const viewState = useSelector(selectViewState) // RSU layer local state variables const [selectedRsuCount, setSelectedRsuCount] = useState(null) @@ -763,7 +765,7 @@ function MapPage(props: MapPageProps) { mapboxAccessToken={EnvironmentVars.MAPBOX_TOKEN} mapStyle={mbStyle as mapboxgl.Style} style={{ width: '100%', height: '100%' }} - onMove={(evt) => setViewState(evt.viewState)} + onMove={(evt) => dispatch(setMapViewState(evt.viewState))} onClick={(e) => { if (addGeoMsgPoint) { addGeoMsgPointToCoordinates(e.lngLat) diff --git a/webapp/src/pages/mapSlice.tsx b/webapp/src/pages/mapSlice.tsx new file mode 100644 index 00000000..21fd48a5 --- /dev/null +++ b/webapp/src/pages/mapSlice.tsx @@ -0,0 +1,25 @@ +import { createSlice } from '@reduxjs/toolkit' +import EnvironmentVars from '../EnvironmentVars' +import { RootState } from '../store' + +const initialState = EnvironmentVars.getMapboxInitViewState() + +export const mapSlice = createSlice({ + name: 'map', + initialState: { + mapViewState: { + ...initialState, + }, + }, + reducers: { + setMapViewState: (state, action) => { + state.mapViewState = action.payload + }, + }, +}) + +export const { setMapViewState } = mapSlice.actions + +export const selectViewState = (state: RootState) => state.map.mapViewState + +export default mapSlice.reducer diff --git a/webapp/src/store.tsx b/webapp/src/store.tsx index 4bfd8f59..703f8016 100644 --- a/webapp/src/store.tsx +++ b/webapp/src/store.tsx @@ -22,6 +22,7 @@ import menuReducer from './features/menu/menuSlice' import intersectionMapReducer from './features/intersections/map/map-slice' import intersectionMapLayerStyleReducer from './features/intersections/map/map-layer-style-slice' import dataSelectorReducer from './features/intersections/data-selector/dataSelectorSlice' +import mapSliceReducer from './pages/mapSlice' export const setupStore = (preloadedState: any) => { return configureStore({ @@ -49,6 +50,7 @@ export const setupStore = (preloadedState: any) => { intersectionMap: intersectionMapReducer, intersectionMapLayerStyle: intersectionMapLayerStyleReducer, dataSelector: dataSelectorReducer, + map: mapSliceReducer, }, preloadedState, middleware: (getDefaultMiddleware) => From 271026e53f7fba788ad24c66c541baea32ece201 Mon Sep 17 00:00:00 2001 From: Marc Wodahl Date: Mon, 16 Sep 2024 13:21:26 -0600 Subject: [PATCH 02/11] Add RSU Errors functionality to API --- services/api/src/main.py | 2 + services/api/src/middleware.py | 3 +- services/api/src/rsu_error_summary.py | 75 +++++++++ .../api/tests/data/rsu_error_summary_data.py | 17 ++ .../api/tests/src/test_rsu_error_summary.py | 150 ++++++++++++++++++ 5 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 services/api/src/rsu_error_summary.py create mode 100644 services/api/tests/data/rsu_error_summary_data.py create mode 100644 services/api/tests/src/test_rsu_error_summary.py diff --git a/services/api/src/main.py b/services/api/src/main.py index b15f6d5d..f9f93dfb 100644 --- a/services/api/src/main.py +++ b/services/api/src/main.py @@ -27,6 +27,7 @@ from admin_new_org import AdminNewOrg from admin_org import AdminOrg from contact_support import ContactSupportResource +from rsu_error_summary import RSUErrorSummaryResource import smtp_error_handler log_level = os.environ.get("LOGGING_LEVEL", "INFO") @@ -61,6 +62,7 @@ api.add_resource(AdminNotification, "/admin-notification") api.add_resource(AdminNewNotification, "/admin-new-notification") api.add_resource(ContactSupportResource, "/contact-support") +api.add_resource(RSUErrorSummaryResource, "/rsu-error-summary") if __name__ == "__main__": app.run(host="0.0.0.0", port=5000) diff --git a/services/api/src/middleware.py b/services/api/src/middleware.py index 97858c4c..e8ea4e29 100644 --- a/services/api/src/middleware.py +++ b/services/api/src/middleware.py @@ -61,6 +61,7 @@ def get_user_role(token): "/rsu-geo-query": True, "/admin-new-notification": False, "/admin-notification": False, + "/rsu-error-summary": False, } @@ -69,7 +70,7 @@ def check_auth_exempt(method, path): if method == "OPTIONS": return True - exempt_paths = ["/", "/contact-support"] + exempt_paths = ["/", "/contact-support", "/rsu-error-summary"] if path in exempt_paths: return True diff --git a/services/api/src/rsu_error_summary.py b/services/api/src/rsu_error_summary.py new file mode 100644 index 00000000..63d0d878 --- /dev/null +++ b/services/api/src/rsu_error_summary.py @@ -0,0 +1,75 @@ +import logging +import os +from flask import abort, request +from flask_restful import Resource +from marshmallow import Schema +from marshmallow import fields + +from common.emailSender import EmailSender + +class RSUErrorSummarySchema(Schema): + emails = fields.Str(required=True) + subject = fields.Str(required=True) + message = fields.Str(required=True) + +class RSUErrorSummaryResource(Resource): + options_headers = { + "Access-Control-Allow-Origin": os.environ["CORS_DOMAIN"], + "Access-Control-Allow-Headers": "Content-Type,Authorization", + "Access-Control-Allow-Methods": "POST,OPTIONS", + "Access-Control-Max-Age": "3600", + } + + headers = { + "Access-Control-Allow-Origin": os.environ["CORS_DOMAIN"], + "Access-Control-Allow-Headers": "Content-Type,Authorization", + "Access-Control-Allow-Methods": "POST,OPTIONS", + "Content-Type": "application/json", + } + + def options(self): + # CORS support + return ("", 204, self.options_headers) + + def post(self): + logging.debug("RSUErrorSummary POST requested") + # Check for main body values + if not request.json: + logging.error("No JSON body provided") + abort(400) + + self.validate_input(request.json) + + try: + email_addresses = request.json["emails"].split(",") + subject = request.json["subject"] + message = request.json["message"] + + for email_address in email_addresses: + logging.info(f"Sending email to {email_address}") + emailSender = EmailSender( + os.environ["CSM_TARGET_SMTP_SERVER_ADDRESS"], + 587, + ) + emailSender.send( + sender=os.environ["CSM_EMAIL_TO_SEND_FROM"], + recipient=email_address, + subject=subject, + message=message, + replyEmail="", + username=os.environ["CSM_EMAIL_APP_USERNAME"], + password=os.environ["CSM_EMAIL_APP_PASSWORD"], + pretty=True, + ) + except Exception as e: + logging.error(f"Exception encountered: {e}") + abort(500) + return ("", 200, self.headers) + + def validate_input(self, input): + try: + schema = RSUErrorSummarySchema() + schema.load(input) + except Exception as e: + logging.error(f"Exception encountered: {e}") + abort(400) \ No newline at end of file diff --git a/services/api/tests/data/rsu_error_summary_data.py b/services/api/tests/data/rsu_error_summary_data.py new file mode 100644 index 00000000..cce2fd97 --- /dev/null +++ b/services/api/tests/data/rsu_error_summary_data.py @@ -0,0 +1,17 @@ +CSM_EMAIL_TO_SEND_FROM = "test@test.com" +CSM_EMAIL_APP_USERNAME = "testusername" +CSM_EMAIL_APP_PASSWORD = "testpassword" +DEFAULT_CSM_TARGET_SMTP_SERVER_ADDRESS = "smtp.gmail.com" +DEFAULT_CSM_TARGET_SMTP_SERVER_PORT = 587 + + +rsu_error_summary_data_good = { + "subject": "test_subject", + "message": "test_message", + "emails": "some-reply@test.com", +} + +rsu_error_summary_data_bad = { + "subject": "test_subject", + "message": "test_message", +} diff --git a/services/api/tests/src/test_rsu_error_summary.py b/services/api/tests/src/test_rsu_error_summary.py new file mode 100644 index 00000000..f99b8258 --- /dev/null +++ b/services/api/tests/src/test_rsu_error_summary.py @@ -0,0 +1,150 @@ +import os +from unittest.mock import MagicMock + +import api.src.rsu_error_summary as rsu_error_summary +import api.tests.data.rsu_error_summary_data as rsu_error_summary_data + +# RSUErrorSummarySchema class tests --- + +def test_rsu_error_summary_schema(): + # prepare + schema = rsu_error_summary.RSUErrorSummarySchema() + + # execute + exceptionOccurred = False + try: + schema.load(rsu_error_summary_data.rsu_error_summary_data_good) + except Exception as e: + exceptionOccurred = True + + # assert + assert exceptionOccurred == False + + +def test_rsu_error_summary_schema_invalid(): + # prepare + schema = rsu_error_summary.RSUErrorSummarySchema() + + # execute + exceptionOccurred = False + try: + schema.load(rsu_error_summary_data.rsu_error_summary_data_bada) + except Exception as e: + exceptionOccurred = True + + # assert + assert exceptionOccurred == True + + +# RSUErrorSummaryResource class tests --- + +def test_options(): + # prepare + os.environ["CSM_EMAIL_TO_SEND_FROM"] = rsu_error_summary_data.CSM_EMAIL_TO_SEND_FROM + os.environ["CSM_EMAIL_APP_USERNAME"] = rsu_error_summary_data.CSM_EMAIL_APP_USERNAME + os.environ["CSM_EMAIL_APP_PASSWORD"] = rsu_error_summary_data.CSM_EMAIL_APP_PASSWORD + os.environ[ + "CSM_TARGET_SMTP_SERVER_ADDRESS" + ] = rsu_error_summary_data.DEFAULT_CSM_TARGET_SMTP_SERVER_ADDRESS + os.environ["CSM_TARGET_SMTP_SERVER_PORT"] = str(rsu_error_summary_data.DEFAULT_CSM_TARGET_SMTP_SERVER_PORT) + RSUErrorSummaryResource = rsu_error_summary.RSUErrorSummaryResource() + + # execute + result = RSUErrorSummaryResource.options() + + # assert + assert result == ("", 204, RSUErrorSummaryResource.options_headers) + + # cleanup + del os.environ["CSM_EMAIL_TO_SEND_FROM"] + del os.environ["CSM_EMAIL_APP_USERNAME"] + del os.environ["CSM_EMAIL_APP_PASSWORD"] + del os.environ["CSM_TARGET_SMTP_SERVER_ADDRESS"] + del os.environ["CSM_TARGET_SMTP_SERVER_PORT"] + + +def test_post_success(): + # prepare + os.environ["CSM_EMAIL_TO_SEND_FROM"] = rsu_error_summary_data.CSM_EMAIL_TO_SEND_FROM + os.environ["CSM_EMAIL_APP_USERNAME"] = rsu_error_summary_data.CSM_EMAIL_APP_USERNAME + os.environ["CSM_EMAIL_APP_PASSWORD"] = rsu_error_summary_data.CSM_EMAIL_APP_PASSWORD + os.environ[ + "CSM_TARGET_SMTP_SERVER_ADDRESS" + ] = rsu_error_summary_data.DEFAULT_CSM_TARGET_SMTP_SERVER_ADDRESS + os.environ["CSM_TARGET_SMTP_SERVER_PORT"] = str(rsu_error_summary_data.DEFAULT_CSM_TARGET_SMTP_SERVER_PORT) + RSUErrorSummaryResource = rsu_error_summary.RSUErrorSummaryResource() + RSUErrorSummaryResource.validate_input = MagicMock() + RSUErrorSummaryResource.send = MagicMock() + rsu_error_summary.abort = MagicMock() + rsu_error_summary.request = MagicMock() + + # execute + result = RSUErrorSummaryResource.post() + + # assert + assert result == ("", 200, RSUErrorSummaryResource.headers) + + # cleanup + del os.environ["CSM_EMAIL_TO_SEND_FROM"] + del os.environ["CSM_EMAIL_APP_USERNAME"] + del os.environ["CSM_EMAIL_APP_PASSWORD"] + del os.environ["CSM_TARGET_SMTP_SERVER_ADDRESS"] + del os.environ["CSM_TARGET_SMTP_SERVER_PORT"] + + +def test_post_no_json_body(): + # prepare + os.environ["CSM_EMAIL_TO_SEND_FROM"] = rsu_error_summary_data.CSM_EMAIL_TO_SEND_FROM + os.environ["CSM_EMAIL_APP_USERNAME"] = rsu_error_summary_data.CSM_EMAIL_APP_USERNAME + os.environ["CSM_EMAIL_APP_PASSWORD"] = rsu_error_summary_data.CSM_EMAIL_APP_PASSWORD + os.environ[ + "CSM_TARGET_SMTP_SERVER_ADDRESS" + ] = rsu_error_summary_data.DEFAULT_CSM_TARGET_SMTP_SERVER_ADDRESS + os.environ["CSM_TARGET_SMTP_SERVER_PORT"] = str(rsu_error_summary_data.DEFAULT_CSM_TARGET_SMTP_SERVER_PORT) + RSUErrorSummaryResource = rsu_error_summary.RSUErrorSummaryResource() + RSUErrorSummaryResource.validate_input = MagicMock() + RSUErrorSummaryResource.send = MagicMock() + rsu_error_summary.abort = MagicMock() + rsu_error_summary.request = MagicMock() + rsu_error_summary.request.json = None + + # execute + result = RSUErrorSummaryResource.post() + + # assert + assert rsu_error_summary.abort.call_count == 2 + assert result == ("", 200, RSUErrorSummaryResource.headers) + + # cleanup + del os.environ["CSM_EMAIL_TO_SEND_FROM"] + del os.environ["CSM_EMAIL_APP_USERNAME"] + del os.environ["CSM_EMAIL_APP_PASSWORD"] + del os.environ["CSM_TARGET_SMTP_SERVER_ADDRESS"] + del os.environ["CSM_TARGET_SMTP_SERVER_PORT"] + + +def test_validate_input_good(): + # prepare + os.environ["CSM_EMAIL_TO_SEND_FROM"] = rsu_error_summary_data.CSM_EMAIL_TO_SEND_FROM + os.environ["CSM_EMAIL_APP_USERNAME"] = rsu_error_summary_data.CSM_EMAIL_APP_USERNAME + os.environ["CSM_EMAIL_APP_PASSWORD"] = rsu_error_summary_data.CSM_EMAIL_APP_PASSWORD + os.environ[ + "CSM_TARGET_SMTP_SERVER_ADDRESS" + ] = rsu_error_summary_data.DEFAULT_CSM_TARGET_SMTP_SERVER_ADDRESS + os.environ["CSM_TARGET_SMTP_SERVER_PORT"] = str(rsu_error_summary_data.DEFAULT_CSM_TARGET_SMTP_SERVER_PORT) + RSUErrorSummaryResource = rsu_error_summary.RSUErrorSummaryResource() + + # execute + result = RSUErrorSummaryResource.validate_input( + rsu_error_summary_data.rsu_error_summary_data_good + ) + + # assert + assert result == None + + # cleanup + del os.environ["CSM_EMAIL_TO_SEND_FROM"] + del os.environ["CSM_EMAIL_APP_USERNAME"] + del os.environ["CSM_EMAIL_APP_PASSWORD"] + del os.environ["CSM_TARGET_SMTP_SERVER_ADDRESS"] + del os.environ["CSM_TARGET_SMTP_SERVER_PORT"] \ No newline at end of file From 5e2c4c4e81e6420f6e9dedd7ef34f1db6d9149eb Mon Sep 17 00:00:00 2001 From: Marc Wodahl Date: Mon, 16 Sep 2024 13:21:40 -0600 Subject: [PATCH 03/11] RSU Errors page testing updates --- .../src/components/RsuErrorSummary.test.tsx | 20 + .../RsuErrorSummary.test.tsx.snap | 7 + .../features/menu/DisplayRsuErrors.test.tsx | 16 + .../DisplayRsuErrors.test.tsx.snap | 479 ++++++++++++++++++ .../menu/__snapshots__/Menu.test.tsx.snap | 7 + webapp/src/features/menu/menuSlice.test.ts | 6 +- webapp/src/pages/mapSlice.test.tsx | 58 +++ 7 files changed, 591 insertions(+), 2 deletions(-) create mode 100644 webapp/src/components/RsuErrorSummary.test.tsx create mode 100644 webapp/src/components/__snapshots__/RsuErrorSummary.test.tsx.snap create mode 100644 webapp/src/features/menu/DisplayRsuErrors.test.tsx create mode 100644 webapp/src/features/menu/__snapshots__/DisplayRsuErrors.test.tsx.snap create mode 100644 webapp/src/pages/mapSlice.test.tsx diff --git a/webapp/src/components/RsuErrorSummary.test.tsx b/webapp/src/components/RsuErrorSummary.test.tsx new file mode 100644 index 00000000..a0662f3d --- /dev/null +++ b/webapp/src/components/RsuErrorSummary.test.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { render } from '@testing-library/react' +import RsuErrorSummary from './RsuErrorSummary' +import { replaceChaoticIds } from '../utils/test-utils' + +it('should take a snapshot', () => { + const { container } = render( +