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

SIMSBIOHUB-620: Manage System Alerts #1401

Merged
merged 22 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3241b25
Initial migration to add alert table WIP)
NickPhura Sep 27, 2024
d57ecda
Merge remote-tracking branch 'origin/dev' into SIMSBIOHUB-620
NickPhura Oct 1, 2024
9401d50
Merge branch 'dev' of github.com:bcgov/biohubbc into SIMSBIOHUB-620
mauberti-bc Oct 15, 2024
d2ec874
add alert endpoints, services, repos and frontend components copying …
mauberti-bc Oct 16, 2024
1180d69
alerts working
mauberti-bc Oct 16, 2024
b619a20
style alert table
mauberti-bc Oct 17, 2024
2ff34ae
separate requests for active and expired alerts & rename manage users…
mauberti-bc Oct 18, 2024
379ad32
Merge branch 'dev' into SIMSBIOHUB-620
MacQSL Oct 23, 2024
f9a582d
Merge branch 'dev' of github.com:bcgov/biohubbc into SIMSBIOHUB-620
mauberti-bc Oct 30, 2024
a7cc51f
Merge branch 'SIMSBIOHUB-620' of github.com:bcgov/biohubbc into SIMSB…
mauberti-bc Oct 30, 2024
1c1deb3
fix tests and code smells
mauberti-bc Oct 31, 2024
2e745e6
address code smell
mauberti-bc Oct 31, 2024
fa96ae3
add updateAlert
mauberti-bc Oct 31, 2024
1f8e275
Merge branch 'dev' into SIMSBIOHUB-620
NickPhura Nov 12, 2024
9d30501
Merge remote-tracking branch 'origin/dev' into SIMSBIOHUB-620
NickPhura Nov 13, 2024
58a2233
ignore-skip
NickPhura Nov 13, 2024
d64927e
CSS Tweaks
NickPhura Nov 13, 2024
61d3168
Remove commented code
NickPhura Nov 14, 2024
bc2544a
address PR comments
mauberti-bc Nov 14, 2024
6a62ebc
Adjust alert spacing css.
NickPhura Nov 15, 2024
e13643b
Tweak error message
NickPhura Nov 15, 2024
2aab4c8
Merge branch 'dev' into SIMSBIOHUB-620
NickPhura Nov 15, 2024
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
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'],
mauberti-bc marked this conversation as resolved.
Show resolved Hide resolved
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,
mauberti-bc marked this conversation as resolved.
Show resolved Hide resolved
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
Loading