Skip to content

Commit

Permalink
SIMSBIOHUB-620: Manage System Alerts (#1401)
Browse files Browse the repository at this point in the history
* 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
3 people authored Nov 15, 2024
1 parent 365ce92 commit 3b86c70
Show file tree
Hide file tree
Showing 60 changed files with 3,063 additions and 355 deletions.
29 changes: 29 additions & 0 deletions api/src/models/alert-view.ts
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';
83 changes: 83 additions & 0 deletions api/src/openapi/schemas/alert.ts
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']
};
141 changes: 141 additions & 0 deletions api/src/paths/alert/index.test.ts
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');
}
});
});
Loading

0 comments on commit 3b86c70

Please sign in to comment.