2026-04-20 10:26:22 +00:00
|
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
|
|
|
|
|
|
|
|
import { Alert } from '../ts/reception/classes.alert.js';
|
|
|
|
|
import { AlertManager } from '../ts/reception/classes.alertmanager.js';
|
|
|
|
|
import { AlertRule } from '../ts/reception/classes.alertrule.js';
|
|
|
|
|
import { PassportDevice } from '../ts/reception/classes.passportdevice.js';
|
|
|
|
|
import { Role } from '../ts/reception/classes.role.js';
|
|
|
|
|
import { User } from '../ts/reception/classes.user.js';
|
|
|
|
|
|
|
|
|
|
const getNestedValue = (targetArg: any, pathArg: string) => {
|
|
|
|
|
return pathArg.split('.').reduce((currentArg, keyArg) => currentArg?.[keyArg], targetArg);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const matchesQuery = (targetArg: any, queryArg: Record<string, any>) => {
|
|
|
|
|
return Object.entries(queryArg).every(([keyArg, valueArg]) => getNestedValue(targetArg, keyArg) === valueArg);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const createTestAlertManager = () => {
|
|
|
|
|
const alerts = new Map<string, Alert>();
|
|
|
|
|
const alertRules = new Map<string, AlertRule>();
|
|
|
|
|
const users = new Map<string, User>();
|
|
|
|
|
const roles = new Map<string, Role>();
|
|
|
|
|
const passportDevices = new Map<string, PassportDevice>();
|
|
|
|
|
const deliveredHints: string[] = [];
|
|
|
|
|
|
|
|
|
|
const manager = new AlertManager({
|
|
|
|
|
db: { smartdataDb: {} },
|
|
|
|
|
typedrouter: { addTypedRouter: () => undefined },
|
|
|
|
|
jwtManager: {
|
|
|
|
|
verifyJWTAndGetData: async (jwtArg: string) => ({
|
|
|
|
|
data: {
|
|
|
|
|
userId: jwtArg,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
userManager: {
|
|
|
|
|
CUser: {
|
|
|
|
|
getInstance: async (queryArg: Record<string, any>) => {
|
|
|
|
|
return Array.from(users.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
|
|
|
|
},
|
|
|
|
|
getInstances: async () => Array.from(users.values()),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
roleManager: {
|
|
|
|
|
CRole: {
|
|
|
|
|
getInstance: async (queryArg: Record<string, any>) => {
|
|
|
|
|
return Array.from(roles.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
getAllRolesForOrg: async (organizationIdArg: string) =>
|
|
|
|
|
Array.from(roles.values()).filter((roleArg) => roleArg.data.organizationId === organizationIdArg),
|
|
|
|
|
},
|
|
|
|
|
passportManager: {
|
|
|
|
|
authenticatePassportDeviceRequest: async (requestArg: { deviceId: string }) => {
|
|
|
|
|
return passportDevices.get(requestArg.deviceId)!;
|
|
|
|
|
},
|
|
|
|
|
getPassportDevicesForUser: async (userIdArg: string) =>
|
|
|
|
|
Array.from(passportDevices.values()).filter(
|
|
|
|
|
(deviceArg) => deviceArg.data.userId === userIdArg && deviceArg.data.status === 'active'
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
passportPushManager: {
|
|
|
|
|
deliverAlertHint: async (_passportDeviceArg: PassportDevice, alertArg: Alert) => {
|
|
|
|
|
deliveredHints.push(alertArg.data.notification.hintId);
|
|
|
|
|
alertArg.data.notification = {
|
|
|
|
|
...alertArg.data.notification,
|
|
|
|
|
status: 'sent',
|
|
|
|
|
attemptCount: alertArg.data.notification.attemptCount + 1,
|
|
|
|
|
deliveredAt: Date.now(),
|
|
|
|
|
lastError: null,
|
|
|
|
|
};
|
|
|
|
|
await alertArg.save();
|
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
} as any);
|
|
|
|
|
|
|
|
|
|
const originalAlertSave = Alert.prototype.save;
|
|
|
|
|
const originalAlertDelete = Alert.prototype.delete;
|
|
|
|
|
const originalAlertRuleSave = AlertRule.prototype.save;
|
|
|
|
|
const originalAlertRuleDelete = AlertRule.prototype.delete;
|
|
|
|
|
|
|
|
|
|
(Alert.prototype as Alert & { save: () => Promise<void> }).save = async function () {
|
|
|
|
|
alerts.set(this.id, this);
|
|
|
|
|
};
|
|
|
|
|
(Alert.prototype as Alert & { delete: () => Promise<void> }).delete = async function () {
|
|
|
|
|
alerts.delete(this.id);
|
|
|
|
|
};
|
|
|
|
|
(AlertRule.prototype as AlertRule & { save: () => Promise<void> }).save = async function () {
|
|
|
|
|
alertRules.set(this.id, this);
|
|
|
|
|
};
|
|
|
|
|
(AlertRule.prototype as AlertRule & { delete: () => Promise<void> }).delete = async function () {
|
|
|
|
|
alertRules.delete(this.id);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(manager as any).CAlert = {
|
|
|
|
|
getInstance: async (queryArg: Record<string, any>) => {
|
|
|
|
|
return Array.from(alerts.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
|
|
|
|
},
|
|
|
|
|
getInstances: async (queryArg: Record<string, any>) => {
|
|
|
|
|
return Array.from(alerts.values()).filter((docArg) => matchesQuery(docArg, queryArg));
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
(manager as any).CAlertRule = {
|
|
|
|
|
getInstance: async (queryArg: Record<string, any>) => {
|
|
|
|
|
return Array.from(alertRules.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null;
|
|
|
|
|
},
|
|
|
|
|
getInstances: async () => Array.from(alertRules.values()),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
manager,
|
|
|
|
|
alerts,
|
|
|
|
|
alertRules,
|
|
|
|
|
users,
|
|
|
|
|
roles,
|
|
|
|
|
passportDevices,
|
|
|
|
|
deliveredHints,
|
|
|
|
|
restore: () => {
|
|
|
|
|
Alert.prototype.save = originalAlertSave;
|
|
|
|
|
Alert.prototype.delete = originalAlertDelete;
|
|
|
|
|
AlertRule.prototype.save = originalAlertRuleSave;
|
|
|
|
|
AlertRule.prototype.delete = originalAlertRuleDelete;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addUser = (
|
|
|
|
|
usersArg: Map<string, User>,
|
|
|
|
|
optionsArg: { id: string; email: string; isGlobalAdmin?: boolean }
|
|
|
|
|
) => {
|
|
|
|
|
const user = new User();
|
|
|
|
|
user.id = optionsArg.id;
|
|
|
|
|
user.data = {
|
|
|
|
|
name: optionsArg.email,
|
|
|
|
|
username: optionsArg.email,
|
|
|
|
|
email: optionsArg.email,
|
|
|
|
|
status: 'active',
|
|
|
|
|
connectedOrgs: [],
|
|
|
|
|
isGlobalAdmin: optionsArg.isGlobalAdmin,
|
|
|
|
|
};
|
|
|
|
|
usersArg.set(user.id, user);
|
|
|
|
|
return user;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const addPassportDevice = (
|
|
|
|
|
passportDevicesArg: Map<string, PassportDevice>,
|
|
|
|
|
optionsArg: { id: string; userId: string; label: string }
|
|
|
|
|
) => {
|
|
|
|
|
const device = new PassportDevice();
|
|
|
|
|
device.id = optionsArg.id;
|
|
|
|
|
device.data = {
|
|
|
|
|
userId: optionsArg.userId,
|
|
|
|
|
label: optionsArg.label,
|
|
|
|
|
platform: 'ios',
|
|
|
|
|
status: 'active',
|
|
|
|
|
publicKeyAlgorithm: 'p256',
|
|
|
|
|
publicKeyX963Base64: 'public-key',
|
|
|
|
|
capabilities: {
|
|
|
|
|
gps: true,
|
|
|
|
|
nfc: true,
|
|
|
|
|
push: true,
|
|
|
|
|
},
|
|
|
|
|
pushRegistration: {
|
|
|
|
|
provider: 'apns',
|
|
|
|
|
token: `${optionsArg.id}-token`,
|
|
|
|
|
topic: 'global.idp.swiftapp',
|
|
|
|
|
environment: 'development',
|
|
|
|
|
registeredAt: Date.now(),
|
|
|
|
|
},
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
lastSeenAt: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
passportDevicesArg.set(device.id, device);
|
|
|
|
|
return device;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
tap.test('creates global admin access alerts with the built-in fallback rule', async () => {
|
|
|
|
|
const { manager, users, passportDevices, alerts, deliveredHints, restore } = createTestAlertManager();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
addUser(users, { id: 'admin-1', email: 'admin-1@example.com', isGlobalAdmin: true });
|
|
|
|
|
addPassportDevice(passportDevices, { id: 'device-1', userId: 'admin-1', label: 'Admin Phone' });
|
|
|
|
|
|
|
|
|
|
const createdAlerts = await manager.createAlertsForEvent({
|
|
|
|
|
category: 'admin',
|
|
|
|
|
eventType: 'global_admin_access',
|
|
|
|
|
severity: 'high',
|
|
|
|
|
title: 'Global admin console accessed',
|
|
|
|
|
body: 'A global admin accessed the console.',
|
|
|
|
|
actorUserId: 'admin-1',
|
|
|
|
|
relatedEntityType: 'global-admin-console',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(createdAlerts).toHaveLength(1);
|
|
|
|
|
expect(alerts.size).toEqual(1);
|
|
|
|
|
expect(createdAlerts[0].data.notification.status).toEqual('sent');
|
|
|
|
|
expect(deliveredHints).toHaveLength(1);
|
|
|
|
|
} finally {
|
|
|
|
|
restore();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('routes organization-scoped alerts to org admins by rule', async () => {
|
|
|
|
|
const { manager, users, roles, passportDevices, restore } = createTestAlertManager();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
addUser(users, { id: 'owner-1', email: 'owner@example.com' });
|
|
|
|
|
addUser(users, { id: 'viewer-1', email: 'viewer@example.com' });
|
|
|
|
|
addPassportDevice(passportDevices, { id: 'owner-device', userId: 'owner-1', label: 'Owner Phone' });
|
|
|
|
|
|
|
|
|
|
const ownerRole = new Role();
|
|
|
|
|
ownerRole.id = 'role-owner';
|
|
|
|
|
ownerRole.data = {
|
|
|
|
|
userId: 'owner-1',
|
|
|
|
|
organizationId: 'org-1',
|
|
|
|
|
roles: ['owner'],
|
|
|
|
|
};
|
|
|
|
|
roles.set(ownerRole.id, ownerRole);
|
|
|
|
|
|
|
|
|
|
const viewerRole = new Role();
|
|
|
|
|
viewerRole.id = 'role-viewer';
|
|
|
|
|
viewerRole.data = {
|
|
|
|
|
userId: 'viewer-1',
|
|
|
|
|
organizationId: 'org-1',
|
|
|
|
|
roles: ['viewer'],
|
|
|
|
|
};
|
|
|
|
|
roles.set(viewerRole.id, viewerRole);
|
|
|
|
|
|
|
|
|
|
const rule = new AlertRule();
|
|
|
|
|
rule.id = 'org-admin-rule';
|
|
|
|
|
rule.data = {
|
|
|
|
|
scope: 'organization',
|
|
|
|
|
organizationId: 'org-1',
|
|
|
|
|
eventType: 'org_security_notice',
|
|
|
|
|
minimumSeverity: 'medium',
|
|
|
|
|
recipientMode: 'org_admins',
|
|
|
|
|
recipientUserIds: [],
|
|
|
|
|
push: true,
|
|
|
|
|
enabled: true,
|
|
|
|
|
createdByUserId: 'owner-1',
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
updatedAt: Date.now(),
|
|
|
|
|
};
|
|
|
|
|
await rule.save();
|
|
|
|
|
|
|
|
|
|
const createdAlerts = await manager.createAlertsForEvent({
|
|
|
|
|
category: 'security',
|
|
|
|
|
eventType: 'org_security_notice',
|
|
|
|
|
severity: 'high',
|
|
|
|
|
title: 'Organization security event',
|
|
|
|
|
body: 'A sensitive organization event occurred.',
|
|
|
|
|
actorUserId: 'viewer-1',
|
|
|
|
|
organizationId: 'org-1',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(createdAlerts).toHaveLength(1);
|
|
|
|
|
expect(createdAlerts[0].data.recipientUserId).toEqual('owner-1');
|
|
|
|
|
} finally {
|
|
|
|
|
restore();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-20 13:21:28 +00:00
|
|
|
tap.test('uses built-in organization fallback rules for app connection events', async () => {
|
|
|
|
|
const { manager, users, roles, passportDevices, deliveredHints, restore } = createTestAlertManager();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
addUser(users, { id: 'owner-1', email: 'owner@example.com' });
|
|
|
|
|
addPassportDevice(passportDevices, { id: 'owner-device', userId: 'owner-1', label: 'Owner Phone' });
|
|
|
|
|
|
|
|
|
|
const ownerRole = new Role();
|
|
|
|
|
ownerRole.id = 'role-owner';
|
|
|
|
|
ownerRole.data = {
|
|
|
|
|
userId: 'owner-1',
|
|
|
|
|
organizationId: 'org-1',
|
|
|
|
|
roles: ['owner'],
|
|
|
|
|
};
|
|
|
|
|
roles.set(ownerRole.id, ownerRole);
|
|
|
|
|
|
|
|
|
|
const createdAlerts = await manager.createAlertsForEvent({
|
|
|
|
|
category: 'admin',
|
|
|
|
|
eventType: 'org_app_connected',
|
|
|
|
|
severity: 'medium',
|
|
|
|
|
title: 'Organization app connected',
|
|
|
|
|
body: 'A new app was connected.',
|
|
|
|
|
actorUserId: 'owner-1',
|
|
|
|
|
organizationId: 'org-1',
|
|
|
|
|
relatedEntityId: 'app-1',
|
|
|
|
|
relatedEntityType: 'global-app',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(createdAlerts).toHaveLength(1);
|
|
|
|
|
expect(createdAlerts[0].data.recipientUserId).toEqual('owner-1');
|
|
|
|
|
expect(deliveredHints).toHaveLength(1);
|
|
|
|
|
} finally {
|
|
|
|
|
restore();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-20 10:26:22 +00:00
|
|
|
tap.test('lists alerts, resolves hint lookups, and marks alerts seen', async () => {
|
|
|
|
|
const { manager, alerts, restore } = createTestAlertManager();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const alert = new Alert();
|
|
|
|
|
alert.id = 'alert-1';
|
|
|
|
|
alert.data = {
|
|
|
|
|
recipientUserId: 'user-1',
|
|
|
|
|
category: 'security',
|
|
|
|
|
eventType: 'global_admin_access',
|
|
|
|
|
severity: 'high',
|
|
|
|
|
title: 'Important alert',
|
|
|
|
|
body: 'Please inspect this alert.',
|
|
|
|
|
notification: {
|
|
|
|
|
hintId: 'hint-1',
|
|
|
|
|
status: 'sent',
|
|
|
|
|
attemptCount: 1,
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
deliveredAt: Date.now(),
|
|
|
|
|
seenAt: null,
|
|
|
|
|
lastError: null,
|
|
|
|
|
},
|
|
|
|
|
createdAt: Date.now(),
|
|
|
|
|
seenAt: null,
|
|
|
|
|
dismissedAt: null,
|
|
|
|
|
};
|
|
|
|
|
await alert.save();
|
|
|
|
|
|
|
|
|
|
const listedAlerts = await manager.listAlertsForUser('user-1');
|
|
|
|
|
expect(listedAlerts).toHaveLength(1);
|
|
|
|
|
|
|
|
|
|
const hintAlert = await manager.getAlertByHint('user-1', 'hint-1');
|
|
|
|
|
expect(hintAlert?.id).toEqual('alert-1');
|
|
|
|
|
|
|
|
|
|
const seenAlert = await manager.markAlertSeen('user-1', 'hint-1');
|
|
|
|
|
expect(seenAlert.data.notification.status).toEqual('seen');
|
|
|
|
|
expect(seenAlert.data.seenAt).toBeGreaterThan(0);
|
|
|
|
|
expect(alerts.get('alert-1')?.data.notification.status).toEqual('seen');
|
2026-04-20 13:21:28 +00:00
|
|
|
|
|
|
|
|
const dismissedAlert = await manager.dismissAlert('user-1', 'hint-1');
|
|
|
|
|
expect(dismissedAlert.data.dismissedAt).toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
const defaultList = await manager.listAlertsForUser('user-1');
|
|
|
|
|
expect(defaultList).toHaveLength(0);
|
|
|
|
|
|
|
|
|
|
const fullList = await manager.listAlertsForUser('user-1', true);
|
|
|
|
|
expect(fullList).toHaveLength(1);
|
2026-04-20 10:26:22 +00:00
|
|
|
} finally {
|
|
|
|
|
restore();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
export default tap.start();
|