@oneuptime/common 12.0.31 → 12.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/AlertState.ts +0 -2
- package/Models/DatabaseModels/Domain.ts +1 -1
- package/Models/DatabaseModels/Incident.ts +1 -1
- package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/MetricType.ts +0 -2
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
- package/Models/DatabaseModels/MonitorTest.ts +0 -2
- package/Models/DatabaseModels/NetworkDevice.ts +121 -2
- package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
- package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
- package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
- package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
- package/Models/DatabaseModels/StatusPageResource.ts +0 -2
- package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/DatabaseService.ts +39 -9
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/NetworkDeviceRoleService.ts +90 -0
- package/Server/Services/ProjectService.ts +85 -0
- package/Server/Services/UserNotificationSettingService.ts +0 -6
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
- package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
- package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
- package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
- package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
- package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
- package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
- package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
- package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
- package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
- package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
- package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
- package/Tests/Server/Utils/BasicCron.test.ts +283 -0
- package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
- package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
- package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
- package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
- package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
- package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
- package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
- package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
- package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
- package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
- package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
- package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
- package/Tests/Utils/Slug.test.ts +61 -1
- package/Tests/jest.setup.ts +19 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
- package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
- package/Types/Permission.ts +45 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
- package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +7 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
- package/UI/Components/Navbar/NavBar.tsx +62 -13
- package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
- package/UI/Components/SideMenu/SideMenu.tsx +12 -2
- package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
- package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
- package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
- package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
- package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
- package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
- package/Utils/Slug.ts +34 -1
- package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
- package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
- package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +124 -3
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +31 -5
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
- package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
- package/build/dist/Server/Services/ProjectService.js +65 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
- package/build/dist/Types/Permission.js +40 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
- package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
- package/build/dist/Utils/Slug.js +31 -1
- package/build/dist/Utils/Slug.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import UserNotificationSetting from "../../../Models/DatabaseModels/UserNotificationSetting";
|
|
2
|
+
import UserNotificationSettingService from "../../../Server/Services/UserNotificationSettingService";
|
|
3
|
+
import NotificationSettingEventType from "../../../Types/NotificationSetting/NotificationSettingEventType";
|
|
4
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
5
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
6
|
+
import {
|
|
7
|
+
afterEach,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
jest,
|
|
12
|
+
test,
|
|
13
|
+
} from "@jest/globals";
|
|
14
|
+
|
|
15
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
16
|
+
"11111111-1111-4111-8111-111111111111",
|
|
17
|
+
);
|
|
18
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
19
|
+
"33333333-3333-4333-8333-333333333333",
|
|
20
|
+
);
|
|
21
|
+
const USER_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
|
|
22
|
+
const OTHER_USER_ID: ObjectID = new ObjectID(
|
|
23
|
+
"44444444-4444-4444-8444-444444444444",
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const STATUS_CHANGE_EVENT: NotificationSettingEventType =
|
|
27
|
+
NotificationSettingEventType.SEND_MONITOR_STATUS_CHANGED_OWNER_NOTIFICATION;
|
|
28
|
+
|
|
29
|
+
const ENABLED_MONITOR_EVENTS: Array<NotificationSettingEventType> = [
|
|
30
|
+
NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_NO_PROBES_ARE_MONITORING_THE_MONITOR,
|
|
31
|
+
NotificationSettingEventType.SEND_MONITOR_NOTIFICATION_WHEN_PORBE_STATUS_CHANGES,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
interface StoredSetting {
|
|
35
|
+
userId: string;
|
|
36
|
+
projectId: string;
|
|
37
|
+
eventType: NotificationSettingEventType;
|
|
38
|
+
alertByEmail: boolean;
|
|
39
|
+
alertBySMS: boolean;
|
|
40
|
+
alertByCall: boolean;
|
|
41
|
+
alertByPush: boolean;
|
|
42
|
+
alertByWhatsApp: boolean;
|
|
43
|
+
alertByTelegram: boolean;
|
|
44
|
+
alertBySlack: boolean;
|
|
45
|
+
alertByMicrosoftTeams: boolean;
|
|
46
|
+
alertByWebhook: boolean;
|
|
47
|
+
isRoot: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface SettingIdentity {
|
|
51
|
+
userId: unknown;
|
|
52
|
+
projectId: unknown;
|
|
53
|
+
eventType: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function key(data: SettingIdentity): string {
|
|
57
|
+
return `${String(data.userId)}|${String(data.projectId)}|${String(
|
|
58
|
+
data.eventType,
|
|
59
|
+
)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("UserNotificationSettingService monitor defaults", () => {
|
|
63
|
+
let store: Map<string, StoredSetting>;
|
|
64
|
+
let createdSettings: Array<StoredSetting>;
|
|
65
|
+
let queriedSettings: Array<SettingIdentity>;
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
store = new Map<string, StoredSetting>();
|
|
69
|
+
createdSettings = [];
|
|
70
|
+
queriedSettings = [];
|
|
71
|
+
|
|
72
|
+
jest
|
|
73
|
+
.spyOn(UserNotificationSettingService, "countBy")
|
|
74
|
+
.mockImplementation(((args: {
|
|
75
|
+
query: {
|
|
76
|
+
userId: ObjectID;
|
|
77
|
+
projectId: ObjectID;
|
|
78
|
+
eventType: NotificationSettingEventType;
|
|
79
|
+
};
|
|
80
|
+
}): Promise<PositiveNumber> => {
|
|
81
|
+
queriedSettings.push(args.query);
|
|
82
|
+
|
|
83
|
+
return Promise.resolve(
|
|
84
|
+
new PositiveNumber(store.has(key(args.query)) ? 1 : 0),
|
|
85
|
+
);
|
|
86
|
+
}) as never);
|
|
87
|
+
|
|
88
|
+
jest
|
|
89
|
+
.spyOn(UserNotificationSettingService, "create")
|
|
90
|
+
.mockImplementation(((args: {
|
|
91
|
+
data: UserNotificationSetting;
|
|
92
|
+
props: { isRoot?: boolean };
|
|
93
|
+
}): Promise<UserNotificationSetting> => {
|
|
94
|
+
const item: UserNotificationSetting = args.data;
|
|
95
|
+
const storedSetting: StoredSetting = {
|
|
96
|
+
userId: String(item.userId),
|
|
97
|
+
projectId: String(item.projectId),
|
|
98
|
+
eventType: item.eventType as NotificationSettingEventType,
|
|
99
|
+
alertByEmail: Boolean(item.alertByEmail),
|
|
100
|
+
alertBySMS: Boolean(item.alertBySMS),
|
|
101
|
+
alertByCall: Boolean(item.alertByCall),
|
|
102
|
+
alertByPush: Boolean(item.alertByPush),
|
|
103
|
+
alertByWhatsApp: Boolean(item.alertByWhatsApp),
|
|
104
|
+
alertByTelegram: Boolean(item.alertByTelegram),
|
|
105
|
+
alertBySlack: Boolean(item.alertBySlack),
|
|
106
|
+
alertByMicrosoftTeams: Boolean(item.alertByMicrosoftTeams),
|
|
107
|
+
alertByWebhook: Boolean(item.alertByWebhook),
|
|
108
|
+
isRoot: Boolean(args.props.isRoot),
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
createdSettings.push(storedSetting);
|
|
112
|
+
store.set(key(storedSetting), storedSetting);
|
|
113
|
+
|
|
114
|
+
return Promise.resolve(item);
|
|
115
|
+
}) as never);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
jest.restoreAllMocks();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function stored(
|
|
123
|
+
eventType: NotificationSettingEventType,
|
|
124
|
+
userId: ObjectID = USER_ID,
|
|
125
|
+
projectId: ObjectID = PROJECT_ID,
|
|
126
|
+
): StoredSetting | undefined {
|
|
127
|
+
return store.get(key({ userId, projectId, eventType }));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function seed(setting: StoredSetting): void {
|
|
131
|
+
store.set(key(setting), setting);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
test("does not seed monitor status-change notifications for a new membership", async () => {
|
|
135
|
+
await UserNotificationSettingService.addDefaultNotificationSettingsForUser(
|
|
136
|
+
USER_ID,
|
|
137
|
+
PROJECT_ID,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(stored(STATUS_CHANGE_EVENT)).toBeUndefined();
|
|
141
|
+
expect(
|
|
142
|
+
createdSettings.some((setting: StoredSetting) => {
|
|
143
|
+
return setting.eventType === STATUS_CHANGE_EVENT;
|
|
144
|
+
}),
|
|
145
|
+
).toBe(false);
|
|
146
|
+
expect(
|
|
147
|
+
queriedSettings.some((setting: SettingIdentity) => {
|
|
148
|
+
return setting.eventType === STATUS_CHANGE_EVENT;
|
|
149
|
+
}),
|
|
150
|
+
).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("keeps the other monitor defaults enabled by email only", async () => {
|
|
154
|
+
await UserNotificationSettingService.addDefaultNotificationSettingsForUser(
|
|
155
|
+
USER_ID,
|
|
156
|
+
PROJECT_ID,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
for (const eventType of ENABLED_MONITOR_EVENTS) {
|
|
160
|
+
expect(stored(eventType)).toEqual({
|
|
161
|
+
userId: USER_ID.toString(),
|
|
162
|
+
projectId: PROJECT_ID.toString(),
|
|
163
|
+
eventType,
|
|
164
|
+
alertByEmail: true,
|
|
165
|
+
alertBySMS: false,
|
|
166
|
+
alertByCall: false,
|
|
167
|
+
alertByPush: false,
|
|
168
|
+
alertByWhatsApp: false,
|
|
169
|
+
alertByTelegram: false,
|
|
170
|
+
alertBySlack: false,
|
|
171
|
+
alertByMicrosoftTeams: false,
|
|
172
|
+
alertByWebhook: false,
|
|
173
|
+
isRoot: true,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("remains idempotent without recreating the disabled default", async () => {
|
|
179
|
+
await UserNotificationSettingService.addDefaultNotificationSettingsForUser(
|
|
180
|
+
USER_ID,
|
|
181
|
+
PROJECT_ID,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const settingsCreatedByFirstCall: number = createdSettings.length;
|
|
185
|
+
const rowsCreatedByFirstCall: number = store.size;
|
|
186
|
+
|
|
187
|
+
await UserNotificationSettingService.addDefaultNotificationSettingsForUser(
|
|
188
|
+
USER_ID,
|
|
189
|
+
PROJECT_ID,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
expect(createdSettings).toHaveLength(settingsCreatedByFirstCall);
|
|
193
|
+
expect(store.size).toBe(rowsCreatedByFirstCall);
|
|
194
|
+
expect(stored(STATUS_CHANGE_EVENT)).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("preserves an existing explicit status-change preference", async () => {
|
|
198
|
+
const optedInSetting: StoredSetting = {
|
|
199
|
+
userId: USER_ID.toString(),
|
|
200
|
+
projectId: PROJECT_ID.toString(),
|
|
201
|
+
eventType: STATUS_CHANGE_EVENT,
|
|
202
|
+
alertByEmail: false,
|
|
203
|
+
alertBySMS: false,
|
|
204
|
+
alertByCall: false,
|
|
205
|
+
alertByPush: true,
|
|
206
|
+
alertByWhatsApp: false,
|
|
207
|
+
alertByTelegram: false,
|
|
208
|
+
alertBySlack: true,
|
|
209
|
+
alertByMicrosoftTeams: false,
|
|
210
|
+
alertByWebhook: false,
|
|
211
|
+
isRoot: true,
|
|
212
|
+
};
|
|
213
|
+
seed(optedInSetting);
|
|
214
|
+
|
|
215
|
+
await UserNotificationSettingService.addDefaultNotificationSettingsForUser(
|
|
216
|
+
USER_ID,
|
|
217
|
+
PROJECT_ID,
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
expect(stored(STATUS_CHANGE_EVENT)).toEqual(optedInSetting);
|
|
221
|
+
expect(
|
|
222
|
+
createdSettings.some((setting: StoredSetting) => {
|
|
223
|
+
return setting.eventType === STATUS_CHANGE_EVENT;
|
|
224
|
+
}),
|
|
225
|
+
).toBe(false);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("applies the off default independently to every user and project", async () => {
|
|
229
|
+
await UserNotificationSettingService.addDefaultNotificationSettingsForUser(
|
|
230
|
+
USER_ID,
|
|
231
|
+
PROJECT_ID,
|
|
232
|
+
);
|
|
233
|
+
await UserNotificationSettingService.addDefaultNotificationSettingsForUser(
|
|
234
|
+
OTHER_USER_ID,
|
|
235
|
+
OTHER_PROJECT_ID,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(stored(STATUS_CHANGE_EVENT, USER_ID, PROJECT_ID)).toBeUndefined();
|
|
239
|
+
expect(
|
|
240
|
+
stored(STATUS_CHANGE_EVENT, OTHER_USER_ID, OTHER_PROJECT_ID),
|
|
241
|
+
).toBeUndefined();
|
|
242
|
+
|
|
243
|
+
for (const eventType of ENABLED_MONITOR_EVENTS) {
|
|
244
|
+
expect(stored(eventType, USER_ID, PROJECT_ID)).toBeDefined();
|
|
245
|
+
expect(stored(eventType, OTHER_USER_ID, OTHER_PROJECT_ID)).toBeDefined();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -3,10 +3,13 @@ import UserSlackService from "../../../Server/Services/UserSlackService";
|
|
|
3
3
|
import UserMicrosoftTeamsService from "../../../Server/Services/UserMicrosoftTeamsService";
|
|
4
4
|
import WorkspaceUserNotificationService from "../../../Server/Services/WorkspaceUserNotificationService";
|
|
5
5
|
import ProjectCallSMSConfigService from "../../../Server/Services/ProjectCallSMSConfigService";
|
|
6
|
+
import MailService from "../../../Server/Services/MailService";
|
|
7
|
+
import UserEmailService from "../../../Server/Services/UserEmailService";
|
|
6
8
|
import logger from "../../../Server/Utils/Logger";
|
|
7
9
|
import UserNotificationSetting from "../../../Models/DatabaseModels/UserNotificationSetting";
|
|
8
10
|
import UserSlack from "../../../Models/DatabaseModels/UserSlack";
|
|
9
11
|
import UserMicrosoftTeams from "../../../Models/DatabaseModels/UserMicrosoftTeams";
|
|
12
|
+
import UserEmail from "../../../Models/DatabaseModels/UserEmail";
|
|
10
13
|
import { CallRequestMessage } from "../../../Types/Call/CallRequest";
|
|
11
14
|
import { EmailEnvelope } from "../../../Types/Email/EmailMessage";
|
|
12
15
|
import EmailTemplateType from "../../../Types/Email/EmailTemplateType";
|
|
@@ -60,6 +63,8 @@ const TEAMS_USER_ID: string = "entra-object-id-1";
|
|
|
60
63
|
|
|
61
64
|
const EVENT_TYPE: NotificationSettingEventType =
|
|
62
65
|
NotificationSettingEventType.SEND_INCIDENT_CREATED_OWNER_NOTIFICATION;
|
|
66
|
+
const MONITOR_STATUS_CHANGE_EVENT: NotificationSettingEventType =
|
|
67
|
+
NotificationSettingEventType.SEND_MONITOR_STATUS_CHANGED_OWNER_NOTIFICATION;
|
|
63
68
|
|
|
64
69
|
type SendUserNotificationData = Parameters<
|
|
65
70
|
typeof UserNotificationSettingService.sendUserNotification
|
|
@@ -114,6 +119,8 @@ describe("UserNotificationSettingService.sendUserNotification - workspace channe
|
|
|
114
119
|
let findSettings: jest.SpyInstance;
|
|
115
120
|
let findSlacks: jest.SpyInstance;
|
|
116
121
|
let findTeams: jest.SpyInstance;
|
|
122
|
+
let findEmails: jest.SpyInstance;
|
|
123
|
+
let sendMail: jest.SpyInstance;
|
|
117
124
|
let sendDm: jest.SpyInstance;
|
|
118
125
|
let loggerError: jest.SpyInstance;
|
|
119
126
|
|
|
@@ -133,6 +140,16 @@ describe("UserNotificationSettingService.sendUserNotification - workspace channe
|
|
|
133
140
|
.spyOn(ProjectCallSMSConfigService, "getProjectDefaultTwilioConfig")
|
|
134
141
|
.mockResolvedValue(undefined as never);
|
|
135
142
|
|
|
143
|
+
findEmails = jest.spyOn(UserEmailService, "findBy").mockResolvedValue([
|
|
144
|
+
{
|
|
145
|
+
email: "user@example.com",
|
|
146
|
+
} as unknown as UserEmail,
|
|
147
|
+
] as never);
|
|
148
|
+
|
|
149
|
+
sendMail = jest
|
|
150
|
+
.spyOn(MailService, "sendMail")
|
|
151
|
+
.mockResolvedValue(undefined as never);
|
|
152
|
+
|
|
136
153
|
findSlacks = jest.spyOn(UserSlackService, "findBy").mockResolvedValue([
|
|
137
154
|
{
|
|
138
155
|
id: new ObjectID("44444444-4444-4444-8444-444444444444"),
|
|
@@ -251,13 +268,37 @@ describe("UserNotificationSettingService.sendUserNotification - workspace channe
|
|
|
251
268
|
expect(findTeams).not.toHaveBeenCalled();
|
|
252
269
|
});
|
|
253
270
|
|
|
254
|
-
test("no
|
|
271
|
+
test("no monitor status-change setting sends nothing on any channel", async () => {
|
|
255
272
|
findSettings.mockResolvedValue(null as never);
|
|
256
273
|
|
|
257
274
|
await UserNotificationSettingService.sendUserNotification(
|
|
258
|
-
notificationData(),
|
|
275
|
+
notificationData({ eventType: MONITOR_STATUS_CHANGE_EVENT }),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
expect(findEmails).not.toHaveBeenCalled();
|
|
279
|
+
expect(sendMail).not.toHaveBeenCalled();
|
|
280
|
+
expect(
|
|
281
|
+
ProjectCallSMSConfigService.getProjectDefaultTwilioConfig,
|
|
282
|
+
).not.toHaveBeenCalled();
|
|
283
|
+
expect(findSlacks).not.toHaveBeenCalled();
|
|
284
|
+
expect(findTeams).not.toHaveBeenCalled();
|
|
285
|
+
expect(sendDm).not.toHaveBeenCalled();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("an explicit monitor status-change email opt-in still delivers", async () => {
|
|
289
|
+
findSettings.mockResolvedValue(
|
|
290
|
+
settingsRow({ alertByEmail: true }) as never,
|
|
259
291
|
);
|
|
260
292
|
|
|
293
|
+
await UserNotificationSettingService.sendUserNotification(
|
|
294
|
+
notificationData({ eventType: MONITOR_STATUS_CHANGE_EVENT }),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
expect(findEmails).toHaveBeenCalledTimes(1);
|
|
298
|
+
expect(sendMail).toHaveBeenCalledTimes(1);
|
|
299
|
+
expect(sendMail.mock.calls[0]?.[0]).toMatchObject({
|
|
300
|
+
toEmail: "user@example.com",
|
|
301
|
+
});
|
|
261
302
|
expect(sendDm).not.toHaveBeenCalled();
|
|
262
303
|
});
|
|
263
304
|
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
import cron from "node-cron";
|
|
3
|
+
import logger from "../../../Server/Utils/Logger";
|
|
4
|
+
import BasicCron from "../../../Server/Utils/BasicCron";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* The wrapper every scheduled job in the app is registered through.
|
|
8
|
+
*
|
|
9
|
+
* Its job is not scheduling — node-cron does that — it is making sure one bad
|
|
10
|
+
* run cannot end the schedule. A cron callback that throws takes its timer
|
|
11
|
+
* with it, so a job that failed once at 03:00 would simply never run again,
|
|
12
|
+
* and nothing would say so: the next morning looks exactly like a night with
|
|
13
|
+
* nothing to do. That is why the tick is wrapped, and it is the behaviour
|
|
14
|
+
* worth pinning here.
|
|
15
|
+
*
|
|
16
|
+
* The startup run is deliberately NOT inside that guard, and the asymmetry is
|
|
17
|
+
* the interesting part of this module — see the last block.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
jest.mock("node-cron", () => {
|
|
21
|
+
return { __esModule: true, default: { schedule: jest.fn() } };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
jest.mock("../../../Server/Utils/Logger", () => {
|
|
25
|
+
return {
|
|
26
|
+
__esModule: true,
|
|
27
|
+
default: {
|
|
28
|
+
debug: jest.fn(),
|
|
29
|
+
info: jest.fn(),
|
|
30
|
+
warn: jest.fn(),
|
|
31
|
+
error: jest.fn(),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const mockedCron: { schedule: jest.Mock } = cron as unknown as {
|
|
37
|
+
schedule: jest.Mock;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const mockedLogger: { debug: jest.Mock; error: jest.Mock } =
|
|
41
|
+
logger as unknown as { debug: jest.Mock; error: jest.Mock };
|
|
42
|
+
|
|
43
|
+
const EVERY_FIVE_MINUTES: string = "*/5 * * * *";
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* The run function BasicCron is handed. jest.Mock's first type parameter is
|
|
47
|
+
* the RETURN type, so this is "a mock taking no arguments and answering with
|
|
48
|
+
* a promise" - named once here rather than restated at every call site.
|
|
49
|
+
*/
|
|
50
|
+
type RunMock = jest.Mock<Promise<void>, []>;
|
|
51
|
+
|
|
52
|
+
function succeedingRun(): RunMock {
|
|
53
|
+
const mock: RunMock = jest.fn() as unknown as RunMock;
|
|
54
|
+
mock.mockImplementation((): Promise<void> => {
|
|
55
|
+
return Promise.resolve();
|
|
56
|
+
});
|
|
57
|
+
return mock;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function failingRun(message: string): RunMock {
|
|
61
|
+
const mock: RunMock = jest.fn() as unknown as RunMock;
|
|
62
|
+
mock.mockImplementation((): Promise<void> => {
|
|
63
|
+
return Promise.reject(new Error(message));
|
|
64
|
+
});
|
|
65
|
+
return mock;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
* BasicCron is declared as returning void but its implementation is async, so
|
|
70
|
+
* every call site here goes through this. That mismatch is not incidental
|
|
71
|
+
* tidying - it is the reason a startup failure reaches callers as an
|
|
72
|
+
* unhandled rejection rather than a thrown error, which the last test covers.
|
|
73
|
+
*/
|
|
74
|
+
function startCron(data: {
|
|
75
|
+
jobName: string;
|
|
76
|
+
schedule: string;
|
|
77
|
+
runOnStartup: boolean;
|
|
78
|
+
runFunction: RunMock;
|
|
79
|
+
}): Promise<void> {
|
|
80
|
+
return BasicCron({
|
|
81
|
+
jobName: data.jobName,
|
|
82
|
+
options: {
|
|
83
|
+
schedule: data.schedule,
|
|
84
|
+
runOnStartup: data.runOnStartup,
|
|
85
|
+
},
|
|
86
|
+
runFunction: data.runFunction as never,
|
|
87
|
+
}) as unknown as Promise<void>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* The callback node-cron was handed. Invoking it is what "the schedule fired"
|
|
92
|
+
* means, without waiting five real minutes for it.
|
|
93
|
+
*/
|
|
94
|
+
function scheduledTick(): () => Promise<void> {
|
|
95
|
+
return mockedCron.schedule.mock.calls[0]![1] as () => Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe("BasicCron", () => {
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
jest.clearAllMocks();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("registration", () => {
|
|
104
|
+
test("registers the job on the schedule it was given", async () => {
|
|
105
|
+
const run: RunMock = succeedingRun();
|
|
106
|
+
|
|
107
|
+
await startCron({
|
|
108
|
+
jobName: "TestJob",
|
|
109
|
+
schedule: EVERY_FIVE_MINUTES,
|
|
110
|
+
runOnStartup: false,
|
|
111
|
+
runFunction: run,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(mockedCron.schedule).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(mockedCron.schedule.mock.calls[0]![0]).toBe(EVERY_FIVE_MINUTES);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/*
|
|
119
|
+
* Registering must not run the job. A job that ran once per process start
|
|
120
|
+
* would fire on every rolling restart and every replica.
|
|
121
|
+
*/
|
|
122
|
+
test("registering alone does not run the job", async () => {
|
|
123
|
+
const run: RunMock = succeedingRun();
|
|
124
|
+
|
|
125
|
+
await startCron({
|
|
126
|
+
jobName: "TestJob",
|
|
127
|
+
schedule: EVERY_FIVE_MINUTES,
|
|
128
|
+
runOnStartup: false,
|
|
129
|
+
runFunction: run,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(run).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("a scheduled run", () => {
|
|
137
|
+
test("runs the job when the schedule fires", async () => {
|
|
138
|
+
const run: RunMock = succeedingRun();
|
|
139
|
+
|
|
140
|
+
await startCron({
|
|
141
|
+
jobName: "TestJob",
|
|
142
|
+
schedule: EVERY_FIVE_MINUTES,
|
|
143
|
+
runOnStartup: false,
|
|
144
|
+
runFunction: run,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await scheduledTick()();
|
|
148
|
+
|
|
149
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/*
|
|
153
|
+
* The whole point of the wrapper. An unhandled throw inside a node-cron
|
|
154
|
+
* callback ends that timer, so the job silently stops running for the
|
|
155
|
+
* lifetime of the process - and a job that never runs looks identical to
|
|
156
|
+
* a job with nothing to do.
|
|
157
|
+
*/
|
|
158
|
+
test("a throwing run is swallowed, so the schedule survives it", async () => {
|
|
159
|
+
const run: RunMock = failingRun("job blew up");
|
|
160
|
+
|
|
161
|
+
await startCron({
|
|
162
|
+
jobName: "TestJob",
|
|
163
|
+
schedule: EVERY_FIVE_MINUTES,
|
|
164
|
+
runOnStartup: false,
|
|
165
|
+
runFunction: run,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await expect(scheduledTick()()).resolves.toBeUndefined();
|
|
169
|
+
expect(mockedLogger.error).toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a failed run does not stop later runs from firing", async () => {
|
|
173
|
+
const run: RunMock = succeedingRun();
|
|
174
|
+
run.mockImplementationOnce((): Promise<void> => {
|
|
175
|
+
return Promise.reject(new Error("first run blew up"));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await startCron({
|
|
179
|
+
jobName: "TestJob",
|
|
180
|
+
schedule: EVERY_FIVE_MINUTES,
|
|
181
|
+
runOnStartup: false,
|
|
182
|
+
runFunction: run,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await scheduledTick()();
|
|
186
|
+
await scheduledTick()();
|
|
187
|
+
|
|
188
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* The job's name is what makes a line in the log attributable to one of
|
|
193
|
+
* many scheduled jobs. Logged without it, a failure says only that
|
|
194
|
+
* something somewhere failed.
|
|
195
|
+
*/
|
|
196
|
+
test("the job's name is in what it logs", async () => {
|
|
197
|
+
const run: RunMock = succeedingRun();
|
|
198
|
+
|
|
199
|
+
await startCron({
|
|
200
|
+
jobName: "DistinctiveJobName",
|
|
201
|
+
schedule: EVERY_FIVE_MINUTES,
|
|
202
|
+
runOnStartup: false,
|
|
203
|
+
runFunction: run,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
await scheduledTick()();
|
|
207
|
+
|
|
208
|
+
const logged: string = mockedLogger.debug.mock.calls
|
|
209
|
+
.map((callArgs: Array<unknown>): string => {
|
|
210
|
+
return String(callArgs[0]);
|
|
211
|
+
})
|
|
212
|
+
.join("\n");
|
|
213
|
+
|
|
214
|
+
expect(logged).toContain("DistinctiveJobName");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("the startup run", () => {
|
|
219
|
+
test("runs the job immediately when asked to", async () => {
|
|
220
|
+
const run: RunMock = succeedingRun();
|
|
221
|
+
|
|
222
|
+
await startCron({
|
|
223
|
+
jobName: "TestJob",
|
|
224
|
+
schedule: EVERY_FIVE_MINUTES,
|
|
225
|
+
runOnStartup: true,
|
|
226
|
+
runFunction: run,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* The startup run is in addition to the schedule, not instead of it - a
|
|
234
|
+
* job that ran once at boot and was never registered would go quiet after
|
|
235
|
+
* the first tick.
|
|
236
|
+
*/
|
|
237
|
+
test("it still registers the recurring schedule", async () => {
|
|
238
|
+
const run: RunMock = succeedingRun();
|
|
239
|
+
|
|
240
|
+
await startCron({
|
|
241
|
+
jobName: "TestJob",
|
|
242
|
+
schedule: EVERY_FIVE_MINUTES,
|
|
243
|
+
runOnStartup: true,
|
|
244
|
+
runFunction: run,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(mockedCron.schedule).toHaveBeenCalledTimes(1);
|
|
248
|
+
|
|
249
|
+
await scheduledTick()();
|
|
250
|
+
|
|
251
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/*
|
|
255
|
+
* DELIBERATE ASYMMETRY, pinned so a later reader does not "fix" it by
|
|
256
|
+
* reflex: unlike a scheduled tick, a startup failure is NOT swallowed. It
|
|
257
|
+
* rejects out of BasicCron.
|
|
258
|
+
*
|
|
259
|
+
* That is the right way round. A scheduled run fails against a live
|
|
260
|
+
* system and the next tick is a genuine retry, so surviving it is what
|
|
261
|
+
* keeps the job alive. A startup run fails against a process that is
|
|
262
|
+
* still coming up - usually because configuration or a dependency is
|
|
263
|
+
* wrong - and a boot that swallowed that would come up looking healthy
|
|
264
|
+
* with a job that never worked.
|
|
265
|
+
*
|
|
266
|
+
* Note the callers' side of this: BasicCronFunction is typed as returning
|
|
267
|
+
* void, so a caller that does not await gets an unhandled rejection
|
|
268
|
+
* rather than a thrown error. Loud either way, which is the intent.
|
|
269
|
+
*/
|
|
270
|
+
test("a startup failure propagates rather than being swallowed", async () => {
|
|
271
|
+
const run: RunMock = failingRun("bad configuration at boot");
|
|
272
|
+
|
|
273
|
+
await expect(
|
|
274
|
+
startCron({
|
|
275
|
+
jobName: "TestJob",
|
|
276
|
+
schedule: EVERY_FIVE_MINUTES,
|
|
277
|
+
runOnStartup: true,
|
|
278
|
+
runFunction: run,
|
|
279
|
+
}),
|
|
280
|
+
).rejects.toThrow("bad configuration at boot");
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|