-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
SIMSBIOHUB-620: Manage System Alerts (#1401)
* Initial migration to add alert table WIP) * add alert endpoints, services, repos and frontend components copying funding sources patterns * alerts working * style alert table * separate requests for active and expired alerts & rename manage users to admin page * fix tests and code smells * address code smell * add updateAlert * ignore-skip * CSS Tweaks * Remove commented code * address PR comments * Adjust alert spacing css. * Tweak error message --------- Co-authored-by: Nick Phura <[email protected]> Co-authored-by: Mac Deluca <[email protected]>
- Loading branch information
1 parent
365ce92
commit 3b86c70
Showing
60 changed files
with
3,063 additions
and
355 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { z } from 'zod'; | ||
|
||
// Define the alert schema | ||
export const IAlert = z.object({ | ||
alert_id: z.number(), | ||
alert_type_id: z.number().int(), | ||
name: z.string(), | ||
message: z.string(), | ||
severity: z.enum(['info', 'success', 'error', 'warning']), | ||
data: z.object({}).nullable(), | ||
record_end_date: z.string().nullable(), | ||
status: z.enum(['active', 'expired']) | ||
}); | ||
|
||
// Infer types from the schema | ||
export type IAlert = z.infer<typeof IAlert>; | ||
export type IAlertCreateObject = Omit<IAlert, 'alert_id' | 'status'>; | ||
export type IAlertUpdateObject = Omit<IAlert, 'status'>; | ||
|
||
// Filter object for viewing alerts | ||
export interface IAlertFilterObject { | ||
expiresBefore?: string; | ||
expiresAfter?: string; | ||
types?: string[]; | ||
} | ||
|
||
// Define severity and status types | ||
export type IAlertSeverity = 'info' | 'success' | 'error' | 'warning'; | ||
export type IAlertStatus = 'active' | 'expired'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import { OpenAPIV3 } from 'openapi-types'; | ||
|
||
/** | ||
* Base schema for system alerts | ||
*/ | ||
const baseSystemAlertSchema: OpenAPIV3.SchemaObject = { | ||
type: 'object', | ||
description: 'Schema defining alerts created by system administrators.', | ||
additionalProperties: false, | ||
properties: { | ||
name: { | ||
description: 'Name to display as the title of the alert', | ||
type: 'string' | ||
}, | ||
message: { | ||
description: 'Message to display on the alert', | ||
type: 'string' | ||
}, | ||
alert_type_id: { | ||
description: 'Type of the alert, controlling how it is displayed.', | ||
type: 'number' | ||
}, | ||
severity: { | ||
description: 'Severity level of the alert', | ||
type: 'string', | ||
enum: ['info', 'success', 'warning', 'error'] | ||
}, | ||
data: { | ||
description: 'Data associated with the alert', | ||
type: 'object', | ||
nullable: true | ||
}, | ||
record_end_date: { | ||
description: 'End date of the alert', | ||
type: 'string', | ||
nullable: true | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Schema for updating system alerts | ||
*/ | ||
export const systemAlertPutSchema: OpenAPIV3.SchemaObject = { | ||
...baseSystemAlertSchema, | ||
required: ['alert_id', 'name', 'message', 'data', 'alert_type_id', 'record_end_date', 'severity'], | ||
additionalProperties: false, | ||
properties: { | ||
...baseSystemAlertSchema.properties, | ||
alert_id: { | ||
type: 'integer', | ||
minimum: 1, | ||
description: 'Primary key of the alert' | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Schema for getting system alerts | ||
*/ | ||
export const systemAlertGetSchema: OpenAPIV3.SchemaObject = { | ||
...baseSystemAlertSchema, | ||
required: ['alert_id', 'name', 'message', 'data', 'alert_type_id', 'record_end_date', 'severity', 'status'], | ||
additionalProperties: false, | ||
properties: { | ||
...systemAlertPutSchema.properties, | ||
status: { | ||
type: 'string', | ||
enum: ['active', 'expired'], | ||
description: | ||
'Status of the alert based on comparing the current date to record_end_date, calculated in the get query.' | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Schema for creating system alerts | ||
*/ | ||
export const systemAlertCreateSchema: OpenAPIV3.SchemaObject = { | ||
...baseSystemAlertSchema, | ||
additionalProperties: false, | ||
required: ['name', 'message', 'data', 'alert_type_id', 'record_end_date', 'severity'] | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import chai, { expect } from 'chai'; | ||
import { afterEach, describe, it } from 'mocha'; | ||
import sinon from 'sinon'; | ||
import sinonChai from 'sinon-chai'; | ||
import { SYSTEM_IDENTITY_SOURCE } from '../../constants/database'; | ||
import { SYSTEM_ROLE } from '../../constants/roles'; | ||
import * as db from '../../database/db'; | ||
import { HTTPError } from '../../errors/http-error'; | ||
import { IAlertSeverity, IAlertStatus } from '../../models/alert-view'; | ||
import { AlertService } from '../../services/alert-service'; | ||
import { getMockDBConnection, getRequestHandlerMocks } from '../../__mocks__/db'; | ||
import { createAlert, getAlerts } from '../alert'; | ||
|
||
chai.use(sinonChai); | ||
|
||
describe('getAlerts', () => { | ||
afterEach(() => { | ||
sinon.restore(); | ||
}); | ||
|
||
describe('as a system user', () => { | ||
it('returns a list of system alerts', async () => { | ||
const mockAlerts = [ | ||
{ | ||
alert_id: 1, | ||
name: 'Alert 1', | ||
message: 'Message 1', | ||
alert_type_id: 1, | ||
severity: 'error' as IAlertSeverity, | ||
status: 'active' as IAlertStatus, | ||
data: null, | ||
record_end_date: null | ||
}, | ||
{ | ||
alert_id: 2, | ||
name: 'Alert 2', | ||
message: 'Message 2', | ||
alert_type_id: 2, | ||
severity: 'error' as IAlertSeverity, | ||
status: 'active' as IAlertStatus, | ||
data: null, | ||
record_end_date: null | ||
} | ||
]; | ||
|
||
const mockDBConnection = getMockDBConnection({ open: sinon.stub(), commit: sinon.stub() }); | ||
sinon.stub(db, 'getDBConnection').returns(mockDBConnection); | ||
sinon.stub(AlertService.prototype, 'getAlerts').resolves(mockAlerts); | ||
|
||
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); | ||
mockReq.system_user = { | ||
system_user_id: 2, | ||
user_identifier: 'username', | ||
identity_source: SYSTEM_IDENTITY_SOURCE.IDIR, | ||
user_guid: '123-456-789', | ||
record_end_date: null, | ||
role_ids: [3], | ||
role_names: [SYSTEM_ROLE.SYSTEM_ADMIN], | ||
email: '[email protected]', | ||
family_name: 'lname', | ||
given_name: 'fname', | ||
display_name: 'test user', | ||
agency: null | ||
}; | ||
|
||
const requestHandler = getAlerts(); | ||
|
||
await requestHandler(mockReq, mockRes, mockNext); | ||
|
||
expect(mockRes.jsonValue).to.eql({ alerts: mockAlerts }); | ||
expect(mockDBConnection.open).to.have.been.calledOnce; | ||
expect(mockDBConnection.commit).to.have.been.calledOnce; | ||
}); | ||
|
||
it('handles errors gracefully', async () => { | ||
const mockDBConnection = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); | ||
sinon.stub(db, 'getDBConnection').returns(mockDBConnection); | ||
sinon.stub(AlertService.prototype, 'getAlerts').rejects(new Error('a test error')); | ||
|
||
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); | ||
const requestHandler = getAlerts(); | ||
|
||
try { | ||
await requestHandler(mockReq, mockRes, mockNext); | ||
expect.fail(); | ||
} catch (actualError) { | ||
expect(mockDBConnection.rollback).to.have.been.calledOnce; | ||
expect(mockDBConnection.release).to.have.been.calledOnce; | ||
expect((actualError as HTTPError).message).to.equal('a test error'); | ||
} | ||
}); | ||
}); | ||
}); | ||
|
||
describe('createAlert', () => { | ||
afterEach(() => { | ||
sinon.restore(); | ||
}); | ||
|
||
it('creates a new alert', async () => { | ||
const mockAlert = { | ||
name: 'New Alert', | ||
message: 'New alert message', | ||
alert_type_id: 1, | ||
severity: 'medium' | ||
}; | ||
|
||
const mockDBConnection = getMockDBConnection({ open: sinon.stub(), commit: sinon.stub() }); | ||
sinon.stub(db, 'getDBConnection').returns(mockDBConnection); | ||
sinon.stub(AlertService.prototype, 'createAlert').resolves(1); | ||
|
||
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); | ||
mockReq.body = mockAlert; | ||
|
||
const requestHandler = createAlert(); | ||
|
||
await requestHandler(mockReq, mockRes, mockNext); | ||
|
||
expect(mockRes.jsonValue).to.eql({ alert_id: 1 }); | ||
expect(mockDBConnection.open).to.have.been.calledOnce; | ||
expect(mockDBConnection.commit).to.have.been.calledOnce; | ||
}); | ||
|
||
it('handles errors gracefully', async () => { | ||
const mockDBConnection = getMockDBConnection({ rollback: sinon.stub(), release: sinon.stub() }); | ||
sinon.stub(db, 'getDBConnection').returns(mockDBConnection); | ||
sinon.stub(AlertService.prototype, 'createAlert').rejects(new Error('a test error')); | ||
|
||
const { mockReq, mockRes, mockNext } = getRequestHandlerMocks(); | ||
const requestHandler = createAlert(); | ||
|
||
try { | ||
await requestHandler(mockReq, mockRes, mockNext); | ||
expect.fail(); | ||
} catch (actualError) { | ||
expect(mockDBConnection.rollback).to.have.been.calledOnce; | ||
expect(mockDBConnection.release).to.have.been.calledOnce; | ||
expect((actualError as HTTPError).message).to.equal('a test error'); | ||
} | ||
}); | ||
}); |
Oops, something went wrong.