Files

353 lines
11 KiB
TypeScript
Raw Permalink Normal View History

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();
}
});
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();
}
});
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');
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);
} finally {
restore();
}
});
export default tap.start();