mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
feat: Improve validation of external alerts API input (#1833)
Co-authored-by: Tom Alexander <teeohhem@gmail.com>
This commit is contained in:
parent
181d8d5409
commit
260c429908
9 changed files with 462 additions and 117 deletions
5
.changeset/silent-ants-shop.md
Normal file
5
.changeset/silent-ants-shop.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/api": patch
|
||||
---
|
||||
|
||||
feat: Improve validation of external alert API input
|
||||
|
|
@ -14,8 +14,10 @@ import Alert, {
|
|||
import Dashboard, { IDashboard } from '@/models/dashboard';
|
||||
import { ISavedSearch, SavedSearch } from '@/models/savedSearch';
|
||||
import { IUser } from '@/models/user';
|
||||
import Webhook from '@/models/webhook';
|
||||
import { Api400Error } from '@/utils/errors';
|
||||
import logger from '@/utils/logger';
|
||||
import { alertSchema } from '@/utils/zod';
|
||||
import { alertSchema, objectIdSchema } from '@/utils/zod';
|
||||
|
||||
export type AlertInput = {
|
||||
id?: string;
|
||||
|
|
@ -45,6 +47,63 @@ export type AlertInput = {
|
|||
};
|
||||
};
|
||||
|
||||
const validateObjectId = (id: string | undefined, message: string) => {
|
||||
if (objectIdSchema.safeParse(id).success === false) {
|
||||
throw new Api400Error(message);
|
||||
}
|
||||
};
|
||||
|
||||
export const validateAlertInput = async (
|
||||
teamId: ObjectId,
|
||||
alertInput: Pick<
|
||||
AlertInput,
|
||||
'source' | 'dashboardId' | 'tileId' | 'savedSearchId' | 'channel'
|
||||
>,
|
||||
) => {
|
||||
if (alertInput.source === AlertSource.TILE) {
|
||||
validateObjectId(alertInput.dashboardId, 'Invalid dashboard ID');
|
||||
|
||||
const dashboard = await Dashboard.findOne({
|
||||
_id: alertInput.dashboardId,
|
||||
team: teamId,
|
||||
});
|
||||
|
||||
if (dashboard == null) {
|
||||
throw new Api400Error('Dashboard not found');
|
||||
}
|
||||
|
||||
if (dashboard.tiles.find(tile => tile.id === alertInput.tileId) == null) {
|
||||
throw new Api400Error('Tile not found');
|
||||
}
|
||||
}
|
||||
|
||||
if (alertInput.source === AlertSource.SAVED_SEARCH) {
|
||||
validateObjectId(alertInput.savedSearchId, 'Invalid saved search ID');
|
||||
|
||||
const savedSearch = await SavedSearch.findOne({
|
||||
_id: alertInput.savedSearchId,
|
||||
team: teamId,
|
||||
});
|
||||
|
||||
if (savedSearch == null) {
|
||||
throw new Api400Error('Saved search not found');
|
||||
}
|
||||
}
|
||||
|
||||
if (alertInput.channel.type === 'webhook') {
|
||||
validateObjectId(alertInput.channel.webhookId, 'Invalid webhook ID');
|
||||
|
||||
if (
|
||||
(await Webhook.findOne({
|
||||
_id: alertInput.channel.webhookId,
|
||||
team: teamId,
|
||||
})) == null
|
||||
) {
|
||||
throw new Api400Error('Webhook not found');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const makeAlert = (alert: AlertInput, userId?: ObjectId): Partial<IAlert> => {
|
||||
return {
|
||||
channel: alert.channel,
|
||||
|
|
@ -75,18 +134,6 @@ export const createAlert = async (
|
|||
alertInput: z.infer<typeof alertSchema>,
|
||||
userId: ObjectId,
|
||||
) => {
|
||||
if (alertInput.source === AlertSource.TILE) {
|
||||
if ((await Dashboard.findById(alertInput.dashboardId)) == null) {
|
||||
throw new Error('Dashboard ID not found');
|
||||
}
|
||||
}
|
||||
|
||||
if (alertInput.source === AlertSource.SAVED_SEARCH) {
|
||||
if ((await SavedSearch.findById(alertInput.savedSearchId)) == null) {
|
||||
throw new Error('Saved Search ID not found');
|
||||
}
|
||||
}
|
||||
|
||||
return new Alert({
|
||||
...makeAlert(alertInput, userId),
|
||||
team: teamId,
|
||||
|
|
|
|||
|
|
@ -637,15 +637,17 @@ export const makeAlertInput = ({
|
|||
interval = '15m',
|
||||
threshold = 8,
|
||||
tileId,
|
||||
webhookId = 'test-webhook-id',
|
||||
}: {
|
||||
dashboardId: string;
|
||||
interval?: AlertInterval;
|
||||
threshold?: number;
|
||||
tileId: string;
|
||||
webhookId?: string;
|
||||
}): Partial<AlertInput> => ({
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: 'test-webhook-id',
|
||||
webhookId,
|
||||
},
|
||||
interval,
|
||||
threshold,
|
||||
|
|
@ -659,14 +661,16 @@ export const makeSavedSearchAlertInput = ({
|
|||
savedSearchId,
|
||||
interval = '15m',
|
||||
threshold = 8,
|
||||
webhookId = 'test-webhook-id',
|
||||
}: {
|
||||
savedSearchId: string;
|
||||
interval?: AlertInterval;
|
||||
threshold?: number;
|
||||
webhookId?: string;
|
||||
}): Partial<AlertInput> => ({
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: 'test-webhook-id',
|
||||
webhookId,
|
||||
},
|
||||
interval,
|
||||
threshold,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
randomMongoId,
|
||||
} from '@/fixtures';
|
||||
import Alert from '@/models/alert';
|
||||
import Webhook, { WebhookDocument, WebhookService } from '@/models/webhook';
|
||||
|
||||
const MOCK_TILES = [makeTile(), makeTile(), makeTile(), makeTile(), makeTile()];
|
||||
|
||||
|
|
@ -18,11 +19,28 @@ const MOCK_DASHBOARD = {
|
|||
|
||||
describe('alerts router', () => {
|
||||
const server = getServer();
|
||||
let agent: Awaited<ReturnType<typeof getLoggedInAgent>>['agent'];
|
||||
let team: Awaited<ReturnType<typeof getLoggedInAgent>>['team'];
|
||||
let user: Awaited<ReturnType<typeof getLoggedInAgent>>['user'];
|
||||
let webhook: WebhookDocument;
|
||||
|
||||
beforeAll(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const result = await getLoggedInAgent(server);
|
||||
agent = result.agent;
|
||||
team = result.team;
|
||||
user = result.user;
|
||||
webhook = await Webhook.create({
|
||||
name: 'Test Webhook',
|
||||
service: WebhookService.Slack,
|
||||
url: 'https://hooks.slack.com/test',
|
||||
team: team._id,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.clearDBs();
|
||||
});
|
||||
|
|
@ -32,7 +50,6 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('can create an alert', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -43,6 +60,7 @@ describe('alerts router', () => {
|
|||
makeAlertInput({
|
||||
dashboardId: dashboard.body.id,
|
||||
tileId: dashboard.body.tiles[0].id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200);
|
||||
|
|
@ -51,7 +69,6 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('can delete an alert', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const resp = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -62,6 +79,7 @@ describe('alerts router', () => {
|
|||
makeAlertInput({
|
||||
dashboardId: resp.body.id,
|
||||
tileId: MOCK_TILES[0].id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200);
|
||||
|
|
@ -71,7 +89,6 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('can update an alert', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -82,6 +99,7 @@ describe('alerts router', () => {
|
|||
makeAlertInput({
|
||||
dashboardId: dashboard.body.id,
|
||||
tileId: MOCK_TILES[0].id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200);
|
||||
|
|
@ -99,7 +117,6 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('preserves createdBy field during updates', async () => {
|
||||
const { agent, user } = await getLoggedInAgent(server);
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -113,6 +130,7 @@ describe('alerts router', () => {
|
|||
dashboardId: dashboard.body.id,
|
||||
tileId: dashboard.body.tiles[0].id,
|
||||
threshold: 5,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200);
|
||||
|
|
@ -146,21 +164,20 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('has alerts attached to dashboards', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
await agent.post('/dashboards').send(MOCK_DASHBOARD).expect(200);
|
||||
const initialDashboards = await agent.get('/dashboards').expect(200);
|
||||
|
||||
// Create alerts for all charts
|
||||
const dashboard = initialDashboards.body[0];
|
||||
await Promise.all(
|
||||
dashboard.tiles.map(tile =>
|
||||
dashboard.tiles.map((tile: { id: string }) =>
|
||||
agent
|
||||
.post('/alerts')
|
||||
.send(
|
||||
makeAlertInput({
|
||||
dashboardId: dashboard._id,
|
||||
tileId: tile.id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200),
|
||||
|
|
@ -176,7 +193,6 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('can silence an alert', async () => {
|
||||
const { agent, user } = await getLoggedInAgent(server);
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -187,6 +203,7 @@ describe('alerts router', () => {
|
|||
makeAlertInput({
|
||||
dashboardId: dashboard.body.id,
|
||||
tileId: dashboard.body.tiles[0].id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200);
|
||||
|
|
@ -209,7 +226,6 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('can unsilence an alert', async () => {
|
||||
const { agent, user } = await getLoggedInAgent(server);
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -220,6 +236,7 @@ describe('alerts router', () => {
|
|||
makeAlertInput({
|
||||
dashboardId: dashboard.body.id,
|
||||
tileId: dashboard.body.tiles[0].id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200);
|
||||
|
|
@ -245,7 +262,6 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('returns silenced info in GET /alerts', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -256,6 +272,7 @@ describe('alerts router', () => {
|
|||
makeAlertInput({
|
||||
dashboardId: dashboard.body.id,
|
||||
tileId: dashboard.body.tiles[0].id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200);
|
||||
|
|
@ -278,7 +295,6 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('prevents silencing an alert that does not exist', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const fakeId = randomMongoId();
|
||||
const mutedUntil = new Date(Date.now() + 3600000).toISOString();
|
||||
|
||||
|
|
@ -289,7 +305,6 @@ describe('alerts router', () => {
|
|||
});
|
||||
|
||||
it('prevents unsilencing an alert that does not exist', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const fakeId = randomMongoId();
|
||||
|
||||
await agent.delete(`/alerts/${fakeId}/silenced`).expect(404); // Should fail
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import mongoose, { Types } from 'mongoose';
|
|||
|
||||
import PresetDashboardFilter from '@/models/presetDashboardFilter';
|
||||
import { Source } from '@/models/source';
|
||||
import Webhook, { WebhookDocument, WebhookService } from '@/models/webhook';
|
||||
|
||||
import {
|
||||
getLoggedInAgent,
|
||||
|
|
@ -25,20 +26,37 @@ const MOCK_DASHBOARD = {
|
|||
tags: ['test'],
|
||||
};
|
||||
|
||||
const MOCK_ALERT = {
|
||||
channel: { type: 'webhook' as const, webhookId: 'abcde' },
|
||||
const makeMockAlert = (webhookId: string) => ({
|
||||
channel: { type: 'webhook' as const, webhookId },
|
||||
interval: '12h' as const,
|
||||
threshold: 1,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
};
|
||||
});
|
||||
|
||||
describe('dashboard router', () => {
|
||||
const server = getServer();
|
||||
let agent: Awaited<ReturnType<typeof getLoggedInAgent>>['agent'];
|
||||
let team: Awaited<ReturnType<typeof getLoggedInAgent>>['team'];
|
||||
let user: Awaited<ReturnType<typeof getLoggedInAgent>>['user'];
|
||||
let webhook: WebhookDocument;
|
||||
|
||||
beforeAll(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const result = await getLoggedInAgent(server);
|
||||
agent = result.agent;
|
||||
team = result.team;
|
||||
user = result.user;
|
||||
webhook = await Webhook.create({
|
||||
name: 'Test Webhook',
|
||||
service: WebhookService.Slack,
|
||||
url: 'https://hooks.slack.com/test',
|
||||
team: team._id,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.clearDBs();
|
||||
});
|
||||
|
|
@ -48,7 +66,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('can create a dashboard', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -61,7 +78,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('can update a dashboard', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -85,7 +101,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('can delete a dashboard', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -96,12 +111,12 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('alerts are created when creating dashboard', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const mockAlert = makeMockAlert(webhook._id.toString());
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send({
|
||||
name: 'Test Dashboard',
|
||||
tiles: [makeTile({ alert: MOCK_ALERT })],
|
||||
tiles: [makeTile({ alert: mockAlert })],
|
||||
tags: [],
|
||||
})
|
||||
.expect(200);
|
||||
|
|
@ -109,14 +124,14 @@ describe('dashboard router', () => {
|
|||
const alerts = await agent.get(`/alerts`).expect(200);
|
||||
expect(alerts.body.data).toMatchObject([
|
||||
{
|
||||
...omit(MOCK_ALERT, 'channel.webhookId'),
|
||||
...omit(mockAlert, 'channel.webhookId'),
|
||||
tileId: dashboard.body.tiles[0].id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('alerts are created when updating dashboard (adding alert to tile)', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const mockAlert = makeMockAlert(webhook._id.toString());
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send(MOCK_DASHBOARD)
|
||||
|
|
@ -126,26 +141,26 @@ describe('dashboard router', () => {
|
|||
.patch(`/dashboards/${dashboard.body.id}`)
|
||||
.send({
|
||||
...dashboard.body,
|
||||
tiles: [...dashboard.body.tiles, makeTile({ alert: MOCK_ALERT })],
|
||||
tiles: [...dashboard.body.tiles, makeTile({ alert: mockAlert })],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const alerts = await agent.get(`/alerts`).expect(200);
|
||||
expect(alerts.body.data).toMatchObject([
|
||||
{
|
||||
...omit(MOCK_ALERT, 'channel.webhookId'),
|
||||
...omit(mockAlert, 'channel.webhookId'),
|
||||
tileId: updatedDashboard.body.tiles[MOCK_DASHBOARD.tiles.length].id,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('alerts are deleted when updating dashboard (deleting tile alert settings)', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const mockAlert = makeMockAlert(webhook._id.toString());
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send({
|
||||
name: 'Test Dashboard',
|
||||
tiles: [makeTile({ alert: MOCK_ALERT })],
|
||||
tiles: [makeTile({ alert: mockAlert })],
|
||||
tags: [],
|
||||
})
|
||||
.expect(200);
|
||||
|
|
@ -163,12 +178,12 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('alerts are deleted when removing alert from tile (keeping tile)', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const mockAlert = makeMockAlert(webhook._id.toString());
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send({
|
||||
name: 'Test Dashboard',
|
||||
tiles: [makeTile({ alert: MOCK_ALERT })],
|
||||
tiles: [makeTile({ alert: mockAlert })],
|
||||
tags: [],
|
||||
})
|
||||
.expect(200);
|
||||
|
|
@ -195,18 +210,18 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('alerts are updated when updating dashboard (updating tile alert settings)', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const mockAlert = makeMockAlert(webhook._id.toString());
|
||||
const dashboard = await agent
|
||||
.post('/dashboards')
|
||||
.send({
|
||||
name: 'Test Dashboard',
|
||||
tiles: [makeTile({ alert: MOCK_ALERT })],
|
||||
tiles: [makeTile({ alert: mockAlert })],
|
||||
tags: [],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const updatedAlert = {
|
||||
...MOCK_ALERT,
|
||||
...mockAlert,
|
||||
threshold: 2,
|
||||
};
|
||||
|
||||
|
|
@ -236,8 +251,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('deletes attached alerts when deleting tiles', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
await agent.post('/dashboards').send(MOCK_DASHBOARD).expect(200);
|
||||
const initialDashboards = await agent.get('/dashboards').expect(200);
|
||||
|
||||
|
|
@ -251,6 +264,7 @@ describe('dashboard router', () => {
|
|||
makeAlertInput({
|
||||
dashboardId: dashboard._id,
|
||||
tileId: tile.id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200),
|
||||
|
|
@ -295,14 +309,15 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('preserves alert creator when different user updates dashboard', async () => {
|
||||
const { agent, user: currentUser } = await getLoggedInAgent(server);
|
||||
const mockAlert = makeMockAlert(webhook._id.toString());
|
||||
const currentUser = user;
|
||||
|
||||
// Arrange: Create dashboard with alert
|
||||
const dashboardResponse = await agent
|
||||
.post('/dashboards')
|
||||
.send({
|
||||
name: 'Test Dashboard',
|
||||
tiles: [makeTile({ alert: MOCK_ALERT })],
|
||||
tiles: [makeTile({ alert: mockAlert })],
|
||||
tags: [],
|
||||
})
|
||||
.expect(200);
|
||||
|
|
@ -325,7 +340,7 @@ describe('dashboard router', () => {
|
|||
// Act: Current user updates the dashboard (modifies alert threshold)
|
||||
const updatedThreshold = 5;
|
||||
const updatedAlert = {
|
||||
...MOCK_ALERT,
|
||||
...mockAlert,
|
||||
threshold: updatedThreshold,
|
||||
};
|
||||
|
||||
|
|
@ -391,8 +406,6 @@ describe('dashboard router', () => {
|
|||
|
||||
describe('GET /preset/:presetDashboard/filters', () => {
|
||||
it('returns preset dashboard filters for a given source', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
// Create a test source
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
|
|
@ -423,8 +436,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns empty array when no filters exist for source', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -439,16 +450,12 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns 400 when sourceId is missing', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
await agent
|
||||
.get(`/dashboards/preset/${PresetDashboard.Services}/filters`)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('returns 400 when sourceId is empty', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
await agent
|
||||
.get(`/dashboards/preset/${PresetDashboard.Services}/filters`)
|
||||
.query({ sourceId: '' })
|
||||
|
|
@ -456,8 +463,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns 400 for invalid preset dashboard type', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -470,12 +475,11 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('does not return filters from other teams in GET', async () => {
|
||||
const { agent: agent1, team: team1 } = await getLoggedInAgent(server);
|
||||
const team2 = new mongoose.Types.ObjectId();
|
||||
|
||||
const source1 = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team1._id,
|
||||
team: team._id,
|
||||
});
|
||||
|
||||
const source2 = await Source.create({
|
||||
|
|
@ -485,7 +489,7 @@ describe('dashboard router', () => {
|
|||
|
||||
await PresetDashboardFilter.create({
|
||||
...MOCK_PRESET_DASHBOARD_FILTER,
|
||||
team: team1._id,
|
||||
team: team._id,
|
||||
source: source1._id,
|
||||
});
|
||||
|
||||
|
|
@ -495,20 +499,18 @@ describe('dashboard router', () => {
|
|||
source: source2._id,
|
||||
});
|
||||
|
||||
const response = await agent1
|
||||
const response = await agent
|
||||
.get(`/dashboards/preset/${PresetDashboard.Services}/filters`)
|
||||
.query({ sourceId: source1._id.toString() })
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toHaveLength(1);
|
||||
expect(response.body[0].team).toEqual(team1._id.toString());
|
||||
expect(response.body[0].team).toEqual(team._id.toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /preset/:presetDashboard/filter', () => {
|
||||
it('creates a new preset dashboard filter', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -549,8 +551,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('creates filter with optional sourceMetricType', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -572,8 +572,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns 400 when filter preset dashboard does not match params', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -594,8 +592,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns 400 when filter is missing required fields', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -614,8 +610,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns 400 when filter body is missing', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
await agent
|
||||
.post(`/dashboards/preset/${PresetDashboard.Services}/filter`)
|
||||
.send({})
|
||||
|
|
@ -625,8 +619,6 @@ describe('dashboard router', () => {
|
|||
|
||||
describe('PUT /preset/:presetDashboard/filter', () => {
|
||||
it('updates an existing preset dashboard filter', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -667,8 +659,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns an error when the filter does not exist', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -690,8 +680,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('updates filter with sourceMetricType', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -722,8 +710,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns 400 when filter preset dashboard does not match params', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -746,8 +732,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns 400 when filter is missing required fields', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
const incompleteFilter = {
|
||||
id: new Types.ObjectId().toString(),
|
||||
name: 'Test Filter',
|
||||
|
|
@ -763,8 +747,6 @@ describe('dashboard router', () => {
|
|||
|
||||
describe('DELETE /preset/:presetDashboard/filter/:id', () => {
|
||||
it('deletes a preset dashboard filter', async () => {
|
||||
const { agent, team } = await getLoggedInAgent(server);
|
||||
|
||||
const source = await Source.create({
|
||||
...MOCK_SOURCE,
|
||||
team: team._id,
|
||||
|
|
@ -793,8 +775,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns 404 when filter does not exist', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
const nonExistentId = new Types.ObjectId().toString();
|
||||
|
||||
await agent
|
||||
|
|
@ -805,16 +785,12 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('returns 400 when id is invalid', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
await agent
|
||||
.delete('/dashboards/preset/services/filter/invalid-id')
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it('returns 400 for invalid preset dashboard type', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
|
||||
const filterId = new Types.ObjectId().toString();
|
||||
|
||||
await agent
|
||||
|
|
@ -823,7 +799,6 @@ describe('dashboard router', () => {
|
|||
});
|
||||
|
||||
it('does not delete filters from other teams', async () => {
|
||||
const { agent: agent } = await getLoggedInAgent(server); // team 1
|
||||
const team2Id = new mongoose.Types.ObjectId();
|
||||
|
||||
const source = await Source.create({
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
makeSavedSearchAlertInput,
|
||||
} from '@/fixtures';
|
||||
import Alert from '@/models/alert';
|
||||
import Webhook, { WebhookDocument, WebhookService } from '@/models/webhook';
|
||||
|
||||
const MOCK_SAVED_SEARCH = {
|
||||
name: 'error',
|
||||
|
|
@ -17,11 +18,28 @@ const MOCK_SAVED_SEARCH = {
|
|||
|
||||
describe('savedSearch router', () => {
|
||||
const server = getServer();
|
||||
let agent: Awaited<ReturnType<typeof getLoggedInAgent>>['agent'];
|
||||
let team: Awaited<ReturnType<typeof getLoggedInAgent>>['team'];
|
||||
let user: Awaited<ReturnType<typeof getLoggedInAgent>>['user'];
|
||||
let webhook: WebhookDocument;
|
||||
|
||||
beforeAll(async () => {
|
||||
await server.start();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const result = await getLoggedInAgent(server);
|
||||
agent = result.agent;
|
||||
team = result.team;
|
||||
user = result.user;
|
||||
webhook = await Webhook.create({
|
||||
name: 'Test Webhook',
|
||||
service: WebhookService.Slack,
|
||||
url: 'https://hooks.slack.com/test',
|
||||
team: team._id,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await server.clearDBs();
|
||||
});
|
||||
|
|
@ -31,7 +49,6 @@ describe('savedSearch router', () => {
|
|||
});
|
||||
|
||||
it('can create a saved search', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const savedSearch = await agent
|
||||
.post('/saved-search')
|
||||
.send(MOCK_SAVED_SEARCH)
|
||||
|
|
@ -41,7 +58,6 @@ describe('savedSearch router', () => {
|
|||
});
|
||||
|
||||
it('cannot create a saved search with empty name', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
await agent
|
||||
.post('/saved-search')
|
||||
.send({ ...MOCK_SAVED_SEARCH, name: ' ' }) // Trimmed string will be empty and invalid
|
||||
|
|
@ -49,7 +65,6 @@ describe('savedSearch router', () => {
|
|||
});
|
||||
|
||||
it('can update a saved search', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const savedSearch = await agent
|
||||
.post('/saved-search')
|
||||
.send(MOCK_SAVED_SEARCH)
|
||||
|
|
@ -62,7 +77,6 @@ describe('savedSearch router', () => {
|
|||
});
|
||||
|
||||
it('cannot update a saved search with empty name', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const savedSearch = await agent
|
||||
.post('/saved-search')
|
||||
.send(MOCK_SAVED_SEARCH)
|
||||
|
|
@ -74,7 +88,6 @@ describe('savedSearch router', () => {
|
|||
});
|
||||
|
||||
it('can update a saved search with undefined name', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const savedSearch = await agent
|
||||
.post('/saved-search')
|
||||
.send(MOCK_SAVED_SEARCH)
|
||||
|
|
@ -87,7 +100,6 @@ describe('savedSearch router', () => {
|
|||
});
|
||||
|
||||
it('can get saved searches', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
await agent.post('/saved-search').send(MOCK_SAVED_SEARCH).expect(200);
|
||||
const savedSearches = await agent.get('/saved-search').expect(200);
|
||||
expect(savedSearches.body.length).toBe(1);
|
||||
|
|
@ -95,7 +107,6 @@ describe('savedSearch router', () => {
|
|||
});
|
||||
|
||||
it('can delete a saved search', async () => {
|
||||
const { agent } = await getLoggedInAgent(server);
|
||||
const savedSearch = await agent
|
||||
.post('/saved-search')
|
||||
.send(MOCK_SAVED_SEARCH)
|
||||
|
|
@ -106,6 +117,7 @@ describe('savedSearch router', () => {
|
|||
.send(
|
||||
makeSavedSearchAlertInput({
|
||||
savedSearchId: savedSearch.body._id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200);
|
||||
|
|
@ -116,8 +128,6 @@ describe('savedSearch router', () => {
|
|||
});
|
||||
|
||||
it('sets createdBy on alerts created from a saved search and populates it in list', async () => {
|
||||
const { agent, user } = await getLoggedInAgent(server);
|
||||
|
||||
// Create a saved search
|
||||
const savedSearch = await agent
|
||||
.post('/saved-search')
|
||||
|
|
@ -130,6 +140,7 @@ describe('savedSearch router', () => {
|
|||
.send(
|
||||
makeSavedSearchAlertInput({
|
||||
savedSearchId: savedSearch.body._id,
|
||||
webhookId: webhook._id.toString(),
|
||||
}),
|
||||
)
|
||||
.expect(200);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
getAlertById,
|
||||
getAlertsEnhanced,
|
||||
updateAlert,
|
||||
validateAlertInput,
|
||||
} from '@/controllers/alerts';
|
||||
import { alertSchema, objectIdSchema } from '@/utils/zod';
|
||||
|
||||
|
|
@ -97,6 +98,7 @@ router.post(
|
|||
}
|
||||
try {
|
||||
const alertInput = req.body;
|
||||
await validateAlertInput(teamId, alertInput);
|
||||
return res.json({
|
||||
data: await createAlert(teamId, alertInput, userId),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { getLoggedInAgent, getServer } from '../../../fixtures';
|
|||
import { AlertSource, AlertThresholdType } from '../../../models/alert';
|
||||
import Alert from '../../../models/alert';
|
||||
import Dashboard from '../../../models/dashboard';
|
||||
import { SavedSearch } from '../../../models/savedSearch';
|
||||
import Webhook, { WebhookService } from '../../../models/webhook';
|
||||
|
||||
// Constants
|
||||
const ALERTS_BASE_URL = '/api/v2/alerts';
|
||||
|
|
@ -39,9 +41,27 @@ describe('External API Alerts', () => {
|
|||
return agent[method](url).set('Authorization', `Bearer ${user?.accessKey}`);
|
||||
};
|
||||
|
||||
// Helper to create a webhook for testing
|
||||
const createTestWebhook = async (options: { teamId?: any } = {}) => {
|
||||
return await Webhook.findOneAndUpdate(
|
||||
{
|
||||
name: 'Test Webhook',
|
||||
service: WebhookService.Slack,
|
||||
team: options.teamId ?? team._id,
|
||||
},
|
||||
{
|
||||
name: 'Test Webhook',
|
||||
service: WebhookService.Slack,
|
||||
url: 'https://hooks.slack.com/test',
|
||||
team: options.teamId ?? team._id,
|
||||
},
|
||||
{ upsert: true, new: true },
|
||||
);
|
||||
};
|
||||
|
||||
// Helper to create a dashboard for testing
|
||||
const createTestDashboard = async (
|
||||
options: { numTiles?: number; name?: string } = {},
|
||||
options: { numTiles?: number; name?: string; teamId?: any } = {},
|
||||
) => {
|
||||
const { numTiles = 1, name = 'Test Dashboard' } = options;
|
||||
|
||||
|
|
@ -58,13 +78,25 @@ describe('External API Alerts', () => {
|
|||
return new Dashboard({
|
||||
name,
|
||||
tiles,
|
||||
team: team._id,
|
||||
team: options.teamId ?? team._id,
|
||||
}).save();
|
||||
};
|
||||
|
||||
// Helper to create a saved search for testing
|
||||
const createTestSavedSearch = async (options: { teamId?: any } = {}) => {
|
||||
return new SavedSearch({
|
||||
name: 'Test Saved Search',
|
||||
where: 'error',
|
||||
whereLanguage: 'lucene',
|
||||
source: new ObjectId(),
|
||||
team: options.teamId ?? team._id,
|
||||
}).save();
|
||||
};
|
||||
|
||||
// Helper to create a test alert via API
|
||||
const createTestAlert = async (overrides = {}) => {
|
||||
const dashboard = await createTestDashboard();
|
||||
const webhook = await createTestWebhook();
|
||||
|
||||
const alertInput = {
|
||||
dashboardId: dashboard._id.toString(),
|
||||
|
|
@ -75,7 +107,7 @@ describe('External API Alerts', () => {
|
|||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: new ObjectId().toString(),
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
name: 'Test Alert',
|
||||
message: 'Test Alert Message',
|
||||
|
|
@ -89,11 +121,12 @@ describe('External API Alerts', () => {
|
|||
return {
|
||||
alert: response.body.data,
|
||||
dashboard,
|
||||
webhook,
|
||||
alertInput,
|
||||
};
|
||||
};
|
||||
|
||||
// Helper to create a test alert directly in the database
|
||||
// Helper to create a test alert directly in the database (bypasses validation)
|
||||
const createTestAlertDirectly = async (overrides = {}) => {
|
||||
return Alert.create({
|
||||
team: team._id,
|
||||
|
|
@ -115,8 +148,9 @@ describe('External API Alerts', () => {
|
|||
|
||||
describe('Response Format', () => {
|
||||
it('should return responses in the expected format', async () => {
|
||||
// Create a test alert with known values
|
||||
// Create a test dashboard and webhook with known values
|
||||
const testDashboard = await createTestDashboard();
|
||||
const testWebhook = await createTestWebhook();
|
||||
const testAlert = {
|
||||
dashboardId: testDashboard._id.toString(),
|
||||
tileId: testDashboard.tiles[0].id,
|
||||
|
|
@ -126,7 +160,7 @@ describe('External API Alerts', () => {
|
|||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: new ObjectId().toString(),
|
||||
webhookId: testWebhook._id.toString(),
|
||||
},
|
||||
name: 'Format Test Alert',
|
||||
message: 'This is a test alert for format verification',
|
||||
|
|
@ -197,11 +231,10 @@ describe('External API Alerts', () => {
|
|||
|
||||
describe('Creating alerts', () => {
|
||||
it('should create an alert', async () => {
|
||||
// Create a test dashboard
|
||||
// Create a test dashboard and webhook
|
||||
const dashboard = await createTestDashboard();
|
||||
const webhook = await createTestWebhook();
|
||||
|
||||
// Create alert data
|
||||
const webhookId = new ObjectId().toString();
|
||||
const alertInput = {
|
||||
dashboardId: dashboard._id.toString(),
|
||||
tileId: dashboard.tiles[0].id,
|
||||
|
|
@ -211,7 +244,7 @@ describe('External API Alerts', () => {
|
|||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: webhookId,
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
name: 'Test Alert',
|
||||
message: 'Test Alert Message',
|
||||
|
|
@ -256,6 +289,7 @@ describe('External API Alerts', () => {
|
|||
it('should create multiple alerts for different tiles', async () => {
|
||||
// Create a dashboard with multiple tiles
|
||||
const dashboard = await createTestDashboard({ numTiles: 3 });
|
||||
const webhook = await createTestWebhook();
|
||||
|
||||
// Store created alert IDs for verification
|
||||
const createdAlertIds: string[] = [];
|
||||
|
|
@ -271,7 +305,7 @@ describe('External API Alerts', () => {
|
|||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: new ObjectId().toString(),
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
name: `Alert for ${tile.id}`,
|
||||
message: `This is an alert for ${tile.id}`,
|
||||
|
|
@ -445,6 +479,253 @@ describe('External API Alerts', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Input validation', () => {
|
||||
describe('webhook validation', () => {
|
||||
it('should reject a non-existent webhook', async () => {
|
||||
const dashboard = await createTestDashboard();
|
||||
|
||||
const alertInput = {
|
||||
dashboardId: dashboard._id.toString(),
|
||||
tileId: dashboard.tiles[0].id,
|
||||
threshold: 100,
|
||||
interval: '1h',
|
||||
source: AlertSource.TILE,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: new ObjectId().toString(), // does not exist
|
||||
},
|
||||
};
|
||||
|
||||
await authRequest('post', ALERTS_BASE_URL).send(alertInput).expect(400);
|
||||
});
|
||||
|
||||
it('should reject a webhook belonging to another team', async () => {
|
||||
const dashboard = await createTestDashboard();
|
||||
const otherTeamWebhook = await createTestWebhook({
|
||||
teamId: new ObjectId(),
|
||||
});
|
||||
|
||||
const alertInput = {
|
||||
dashboardId: dashboard._id.toString(),
|
||||
tileId: dashboard.tiles[0].id,
|
||||
threshold: 100,
|
||||
interval: '1h',
|
||||
source: AlertSource.TILE,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: otherTeamWebhook._id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
await authRequest('post', ALERTS_BASE_URL).send(alertInput).expect(400);
|
||||
});
|
||||
|
||||
it('should reject an update with a webhook belonging to another team', async () => {
|
||||
const { alert, dashboard } = await createTestAlert();
|
||||
const otherTeamWebhook = await createTestWebhook({
|
||||
teamId: new ObjectId(),
|
||||
});
|
||||
|
||||
const updatePayload = {
|
||||
threshold: 200,
|
||||
interval: '1h',
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
source: AlertSource.TILE,
|
||||
dashboardId: dashboard._id.toString(),
|
||||
tileId: dashboard.tiles[0].id,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: otherTeamWebhook._id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
await authRequest('put', `${ALERTS_BASE_URL}/${alert.id}`)
|
||||
.send(updatePayload)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dashboard (TILE source) validation', () => {
|
||||
it('should reject a non-existent dashboard', async () => {
|
||||
const webhook = await createTestWebhook();
|
||||
|
||||
const alertInput = {
|
||||
dashboardId: new ObjectId().toString(), // does not exist
|
||||
tileId: new ObjectId().toString(),
|
||||
threshold: 100,
|
||||
interval: '1h',
|
||||
source: AlertSource.TILE,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
await authRequest('post', ALERTS_BASE_URL).send(alertInput).expect(400);
|
||||
});
|
||||
|
||||
it('should reject a dashboard belonging to another team', async () => {
|
||||
const webhook = await createTestWebhook();
|
||||
const otherTeamDashboard = await createTestDashboard({
|
||||
teamId: new ObjectId(),
|
||||
});
|
||||
|
||||
const alertInput = {
|
||||
dashboardId: otherTeamDashboard._id.toString(),
|
||||
tileId: otherTeamDashboard.tiles[0].id,
|
||||
threshold: 100,
|
||||
interval: '1h',
|
||||
source: AlertSource.TILE,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
await authRequest('post', ALERTS_BASE_URL).send(alertInput).expect(400);
|
||||
});
|
||||
|
||||
it('should reject an update with a dashboard belonging to another team', async () => {
|
||||
const { alert, webhook } = await createTestAlert();
|
||||
const otherTeamDashboard = await createTestDashboard({
|
||||
teamId: new ObjectId(),
|
||||
});
|
||||
|
||||
const updatePayload = {
|
||||
threshold: 200,
|
||||
interval: '1h',
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
source: AlertSource.TILE,
|
||||
dashboardId: otherTeamDashboard._id.toString(),
|
||||
tileId: otherTeamDashboard.tiles[0].id,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
await authRequest('put', `${ALERTS_BASE_URL}/${alert.id}`)
|
||||
.send(updatePayload)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saved search (SAVED_SEARCH source) validation', () => {
|
||||
it('should reject a non-existent saved search', async () => {
|
||||
const webhook = await createTestWebhook();
|
||||
|
||||
const alertInput = {
|
||||
savedSearchId: new ObjectId().toString(), // does not exist
|
||||
threshold: 100,
|
||||
interval: '1h',
|
||||
source: AlertSource.SAVED_SEARCH,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
await authRequest('post', ALERTS_BASE_URL).send(alertInput).expect(400);
|
||||
});
|
||||
|
||||
it('should reject a saved search belonging to another team', async () => {
|
||||
const webhook = await createTestWebhook();
|
||||
const otherTeamSavedSearch = await createTestSavedSearch({
|
||||
teamId: new ObjectId(),
|
||||
});
|
||||
|
||||
const alertInput = {
|
||||
savedSearchId: otherTeamSavedSearch._id.toString(),
|
||||
threshold: 100,
|
||||
interval: '1h',
|
||||
source: AlertSource.SAVED_SEARCH,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
await authRequest('post', ALERTS_BASE_URL).send(alertInput).expect(400);
|
||||
});
|
||||
|
||||
it('should create an alert with a valid saved search belonging to the team', async () => {
|
||||
const webhook = await createTestWebhook();
|
||||
const savedSearch = await createTestSavedSearch();
|
||||
|
||||
const alertInput = {
|
||||
savedSearchId: savedSearch._id.toString(),
|
||||
threshold: 100,
|
||||
interval: '1h',
|
||||
source: AlertSource.SAVED_SEARCH,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
name: 'Saved Search Alert',
|
||||
};
|
||||
|
||||
const response = await authRequest('post', ALERTS_BASE_URL)
|
||||
.send(alertInput)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.data.source).toBe(AlertSource.SAVED_SEARCH);
|
||||
expect(response.body.data.savedSearchId).toBe(
|
||||
savedSearch._id.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should reject an update with a saved search belonging to another team', async () => {
|
||||
const savedSearch = await createTestSavedSearch();
|
||||
const webhook = await createTestWebhook();
|
||||
|
||||
// Create a saved search alert first
|
||||
const createResponse = await authRequest('post', ALERTS_BASE_URL)
|
||||
.send({
|
||||
savedSearchId: savedSearch._id.toString(),
|
||||
threshold: 100,
|
||||
interval: '1h',
|
||||
source: AlertSource.SAVED_SEARCH,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const otherTeamSavedSearch = await createTestSavedSearch({
|
||||
teamId: new ObjectId(),
|
||||
});
|
||||
|
||||
const updatePayload = {
|
||||
savedSearchId: otherTeamSavedSearch._id.toString(),
|
||||
threshold: 200,
|
||||
interval: '1h',
|
||||
source: AlertSource.SAVED_SEARCH,
|
||||
thresholdType: AlertThresholdType.ABOVE,
|
||||
channel: {
|
||||
type: 'webhook',
|
||||
webhookId: webhook._id.toString(),
|
||||
},
|
||||
};
|
||||
|
||||
await authRequest(
|
||||
'put',
|
||||
`${ALERTS_BASE_URL}/${createResponse.body.data.id}`,
|
||||
)
|
||||
.send(updatePayload)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
it('should require authentication', async () => {
|
||||
// Create an unauthenticated agent
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
getAlertById,
|
||||
getAlerts,
|
||||
updateAlert,
|
||||
validateAlertInput,
|
||||
} from '@/controllers/alerts';
|
||||
import { validateRequestWithEnhancedErrors as validateRequest } from '@/utils/enhancedErrors';
|
||||
import { translateAlertDocumentToExternalAlert } from '@/utils/externalApi';
|
||||
|
|
@ -404,7 +405,9 @@ router.post(
|
|||
return res.sendStatus(403);
|
||||
}
|
||||
try {
|
||||
const alertInput = req.body;
|
||||
const alertInput = alertSchema.parse(req.body);
|
||||
await validateAlertInput(teamId, alertInput);
|
||||
|
||||
const createdAlert = await createAlert(teamId, alertInput, userId);
|
||||
|
||||
return res.json({
|
||||
|
|
@ -496,7 +499,9 @@ router.put(
|
|||
}
|
||||
const { id } = req.params;
|
||||
|
||||
const alertInput = req.body;
|
||||
const alertInput = alertSchema.parse(req.body);
|
||||
await validateAlertInput(teamId, alertInput);
|
||||
|
||||
const alert = await updateAlert(id, teamId, alertInput);
|
||||
|
||||
if (alert == null) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue