@oneuptime/common 12.0.29 → 12.0.30
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/Index.ts +11 -0
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
- package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
- package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
- package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
- package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
- package/Server/API/OnCallCalendarAPI.ts +2194 -0
- package/Server/EnvironmentConfig.ts +138 -0
- package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
- package/Server/Services/Index.ts +12 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
- package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
- package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
- package/Server/Services/TeamMemberService.ts +489 -1
- package/Server/Services/TeamService.ts +50 -0
- package/Server/Services/UserNotificationSettingService.ts +44 -1
- package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
- package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
- package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
- package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
- package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
- package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
- package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
- package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
- package/Server/Utils/Response.ts +134 -0
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
- package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
- package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
- package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
- package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
- package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
- package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
- package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
- package/Tests/Server/API/Helpers.ts +8 -0
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
- package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
- package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
- package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
- package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
- package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
- package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
- package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
- package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
- package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
- package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
- package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
- package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
- package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
- package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
- package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
- package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
- package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
- package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Types/Calendar/ICalendar.ts +442 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
- package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
- package/Types/OnCallDutyPolicy/Layer.ts +190 -35
- package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
- package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
- package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
- package/build/dist/Models/DatabaseModels/Index.js +11 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +102 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.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/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
- package/build/dist/Server/Services/Index.js +12 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/TeamMemberService.js +396 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamService.js +43 -0
- package/build/dist/Server/Services/TeamService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +97 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +9 -1
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
- package/build/dist/Types/Calendar/ICalendar.js +290 -0
- package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* PasswordHash carries a pre-existing TS5.9 diagnostic that fails any suite
|
|
5
|
+
* whose runtime require graph reaches it, and DatabaseService - the base class
|
|
6
|
+
* of every service below - imports it.
|
|
7
|
+
*/
|
|
8
|
+
jest.mock("../../../Server/Utils/PasswordHash", () => {
|
|
9
|
+
return {
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: {
|
|
12
|
+
hash: jest.fn(),
|
|
13
|
+
verify: jest.fn(),
|
|
14
|
+
generateSalt: jest.fn(),
|
|
15
|
+
needsUpgrade: jest.fn(),
|
|
16
|
+
applyPepper: jest.fn(),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
import UserOnCallShiftReminderService from "../../../Server/Services/UserOnCallShiftReminderService";
|
|
22
|
+
import UserOnCallShiftReminderLogService from "../../../Server/Services/UserOnCallShiftReminderLogService";
|
|
23
|
+
import UserOnCallShiftReminder, {
|
|
24
|
+
MAX_MINUTES_BEFORE_SHIFT,
|
|
25
|
+
MIN_MINUTES_BEFORE_SHIFT,
|
|
26
|
+
} from "../../../Models/DatabaseModels/UserOnCallShiftReminder";
|
|
27
|
+
import UserOnCallShiftReminderLog, {
|
|
28
|
+
UserOnCallShiftReminderLogKind,
|
|
29
|
+
} from "../../../Models/DatabaseModels/UserOnCallShiftReminderLog";
|
|
30
|
+
import CreateBy from "../../../Server/Types/Database/CreateBy";
|
|
31
|
+
import DeleteBy from "../../../Server/Types/Database/DeleteBy";
|
|
32
|
+
import UpdateBy from "../../../Server/Types/Database/UpdateBy";
|
|
33
|
+
import {
|
|
34
|
+
OnCreate,
|
|
35
|
+
OnDelete,
|
|
36
|
+
OnUpdate,
|
|
37
|
+
} from "../../../Server/Types/Database/Hooks";
|
|
38
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
39
|
+
import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
|
|
40
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
41
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
42
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
43
|
+
import { getJestSpyOn } from "../../Spy";
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* Fixture overrides may set a column to undefined on purpose (a missing
|
|
47
|
+
* field is the case under test), which Partial<Model> forbids under
|
|
48
|
+
* exactOptionalPropertyTypes.
|
|
49
|
+
*/
|
|
50
|
+
type Loose<TModel> = { [K in keyof TModel]?: TModel[K] | undefined };
|
|
51
|
+
|
|
52
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
53
|
+
"11111111-1111-4111-8111-111111111111",
|
|
54
|
+
);
|
|
55
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
56
|
+
"22222222-2222-4222-8222-222222222222",
|
|
57
|
+
);
|
|
58
|
+
const USER_ID: ObjectID = new ObjectID("33333333-3333-4333-8333-333333333333");
|
|
59
|
+
const OTHER_USER_ID: ObjectID = new ObjectID(
|
|
60
|
+
"77777777-7777-4777-8777-777777777777",
|
|
61
|
+
);
|
|
62
|
+
const SCHEDULE_ID: ObjectID = new ObjectID(
|
|
63
|
+
"44444444-4444-4444-8444-444444444444",
|
|
64
|
+
);
|
|
65
|
+
const ROW_ID: ObjectID = new ObjectID("55555555-5555-4555-8555-555555555555");
|
|
66
|
+
|
|
67
|
+
type ReminderHooks = {
|
|
68
|
+
onBeforeCreate: (
|
|
69
|
+
createBy: CreateBy<UserOnCallShiftReminder>,
|
|
70
|
+
) => Promise<OnCreate<UserOnCallShiftReminder>>;
|
|
71
|
+
onBeforeUpdate: (
|
|
72
|
+
updateBy: UpdateBy<UserOnCallShiftReminder>,
|
|
73
|
+
) => Promise<OnUpdate<UserOnCallShiftReminder>>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type LogHooks = {
|
|
77
|
+
onBeforeCreate: (
|
|
78
|
+
createBy: CreateBy<UserOnCallShiftReminderLog>,
|
|
79
|
+
) => Promise<OnCreate<UserOnCallShiftReminderLog>>;
|
|
80
|
+
onBeforeUpdate: (
|
|
81
|
+
updateBy: UpdateBy<UserOnCallShiftReminderLog>,
|
|
82
|
+
) => Promise<OnUpdate<UserOnCallShiftReminderLog>>;
|
|
83
|
+
onBeforeDelete: (
|
|
84
|
+
deleteBy: DeleteBy<UserOnCallShiftReminderLog>,
|
|
85
|
+
) => Promise<OnDelete<UserOnCallShiftReminderLog>>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const reminderHooks: ReminderHooks =
|
|
89
|
+
UserOnCallShiftReminderService as unknown as ReminderHooks;
|
|
90
|
+
|
|
91
|
+
const logHooks: LogHooks =
|
|
92
|
+
UserOnCallShiftReminderLogService as unknown as LogHooks;
|
|
93
|
+
|
|
94
|
+
const validateLead: (value: unknown) => number = (value: unknown): number => {
|
|
95
|
+
return (
|
|
96
|
+
UserOnCallShiftReminderService.constructor as unknown as {
|
|
97
|
+
validateMinutesBeforeShift: (value: unknown) => number;
|
|
98
|
+
}
|
|
99
|
+
).validateMinutesBeforeShift(value);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const logStatics: {
|
|
103
|
+
truncateToMinute: (date: Date) => Date;
|
|
104
|
+
validateMinutesBeforeShift: (value: unknown) => number;
|
|
105
|
+
} = UserOnCallShiftReminderLogService.constructor as unknown as {
|
|
106
|
+
truncateToMinute: (date: Date) => Date;
|
|
107
|
+
validateMinutesBeforeShift: (value: unknown) => number;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
function reminder(
|
|
111
|
+
overrides: Loose<UserOnCallShiftReminder> = {},
|
|
112
|
+
): UserOnCallShiftReminder {
|
|
113
|
+
const model: UserOnCallShiftReminder = new UserOnCallShiftReminder();
|
|
114
|
+
model.projectId = PROJECT_ID;
|
|
115
|
+
model.userId = USER_ID;
|
|
116
|
+
model.minutesBeforeShift = 60;
|
|
117
|
+
Object.assign(model, overrides);
|
|
118
|
+
return model;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function userCreate<TModel extends BaseModel>(
|
|
122
|
+
data: TModel,
|
|
123
|
+
props: Record<string, unknown> = {},
|
|
124
|
+
): CreateBy<TModel> {
|
|
125
|
+
return {
|
|
126
|
+
data,
|
|
127
|
+
props: { isRoot: false, userId: USER_ID, tenantId: PROJECT_ID, ...props },
|
|
128
|
+
} as unknown as CreateBy<TModel>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function rootCreate<TModel extends BaseModel>(
|
|
132
|
+
data: TModel,
|
|
133
|
+
props: Record<string, unknown> = {},
|
|
134
|
+
): CreateBy<TModel> {
|
|
135
|
+
return {
|
|
136
|
+
data,
|
|
137
|
+
props: { isRoot: true, ...props },
|
|
138
|
+
} as unknown as CreateBy<TModel>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function update<TModel extends BaseModel>(
|
|
142
|
+
data: Loose<TModel>,
|
|
143
|
+
isRoot: boolean = false,
|
|
144
|
+
): UpdateBy<TModel> {
|
|
145
|
+
return {
|
|
146
|
+
query: { _id: ROW_ID },
|
|
147
|
+
data,
|
|
148
|
+
props: { isRoot },
|
|
149
|
+
limit: 1,
|
|
150
|
+
skip: 0,
|
|
151
|
+
} as unknown as UpdateBy<TModel>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function noDuplicates(): jest.SpyInstance {
|
|
155
|
+
return getJestSpyOn(
|
|
156
|
+
UserOnCallShiftReminderService,
|
|
157
|
+
"countBy",
|
|
158
|
+
).mockResolvedValue(new PositiveNumber(0));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
afterEach(() => {
|
|
162
|
+
jest.restoreAllMocks();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("UserOnCallShiftReminderService.onBeforeCreate", () => {
|
|
166
|
+
test("accepts a well-formed reminder from its owner", async () => {
|
|
167
|
+
noDuplicates();
|
|
168
|
+
|
|
169
|
+
const onCreate: OnCreate<UserOnCallShiftReminder> =
|
|
170
|
+
await reminderHooks.onBeforeCreate(userCreate(reminder()));
|
|
171
|
+
|
|
172
|
+
expect(onCreate.createBy.data.projectId).toBe(PROJECT_ID);
|
|
173
|
+
expect(onCreate.createBy.data.userId).toBe(USER_ID);
|
|
174
|
+
expect(onCreate.createBy.data.minutesBeforeShift).toBe(60);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("defaults the owner to the session user so the page can post just the lead", async () => {
|
|
178
|
+
noDuplicates();
|
|
179
|
+
|
|
180
|
+
const onCreate: OnCreate<UserOnCallShiftReminder> =
|
|
181
|
+
await reminderHooks.onBeforeCreate(
|
|
182
|
+
userCreate(reminder({ userId: undefined })),
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
expect(onCreate.createBy.data.userId).toBe(USER_ID);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("does not override an explicit owner (ownership is enforced downstream)", async () => {
|
|
189
|
+
noDuplicates();
|
|
190
|
+
|
|
191
|
+
const onCreate: OnCreate<UserOnCallShiftReminder> =
|
|
192
|
+
await reminderHooks.onBeforeCreate(
|
|
193
|
+
userCreate(reminder({ userId: OTHER_USER_ID })),
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(onCreate.createBy.data.userId).toBe(OTHER_USER_ID);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("requires an owner when there is no session user either", async () => {
|
|
200
|
+
noDuplicates();
|
|
201
|
+
|
|
202
|
+
await expect(
|
|
203
|
+
reminderHooks.onBeforeCreate(rootCreate(reminder({ userId: undefined }))),
|
|
204
|
+
).rejects.toThrow(BadDataException);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("takes the project from the tenant and requires one from somewhere", async () => {
|
|
208
|
+
noDuplicates();
|
|
209
|
+
|
|
210
|
+
const onCreate: OnCreate<UserOnCallShiftReminder> =
|
|
211
|
+
await reminderHooks.onBeforeCreate(
|
|
212
|
+
userCreate(reminder({ projectId: undefined }), {
|
|
213
|
+
tenantId: OTHER_PROJECT_ID,
|
|
214
|
+
}),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
expect(onCreate.createBy.data.projectId).toBe(OTHER_PROJECT_ID);
|
|
218
|
+
|
|
219
|
+
await expect(
|
|
220
|
+
reminderHooks.onBeforeCreate(
|
|
221
|
+
userCreate(reminder({ projectId: undefined }), {
|
|
222
|
+
tenantId: undefined,
|
|
223
|
+
}),
|
|
224
|
+
),
|
|
225
|
+
).rejects.toThrow(BadDataException);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("normalises a numeric-string lead the form posted", async () => {
|
|
229
|
+
noDuplicates();
|
|
230
|
+
|
|
231
|
+
const onCreate: OnCreate<UserOnCallShiftReminder> =
|
|
232
|
+
await reminderHooks.onBeforeCreate(
|
|
233
|
+
userCreate(
|
|
234
|
+
reminder({ minutesBeforeShift: "1440" as unknown as number }),
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(onCreate.createBy.data.minutesBeforeShift).toBe(1440);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("rejects a lead outside 15 minutes ... two weeks, or not a whole number", async () => {
|
|
242
|
+
noDuplicates();
|
|
243
|
+
|
|
244
|
+
for (const bad of [
|
|
245
|
+
undefined,
|
|
246
|
+
0,
|
|
247
|
+
14,
|
|
248
|
+
MAX_MINUTES_BEFORE_SHIFT + 1,
|
|
249
|
+
-60,
|
|
250
|
+
30.5,
|
|
251
|
+
Number.NaN,
|
|
252
|
+
"soon" as unknown as number,
|
|
253
|
+
]) {
|
|
254
|
+
await expect(
|
|
255
|
+
reminderHooks.onBeforeCreate(
|
|
256
|
+
userCreate(reminder({ minutesBeforeShift: bad })),
|
|
257
|
+
),
|
|
258
|
+
).rejects.toThrow(BadDataException);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("accepts the exact bounds", async () => {
|
|
263
|
+
noDuplicates();
|
|
264
|
+
|
|
265
|
+
const min: OnCreate<UserOnCallShiftReminder> =
|
|
266
|
+
await reminderHooks.onBeforeCreate(
|
|
267
|
+
userCreate(reminder({ minutesBeforeShift: MIN_MINUTES_BEFORE_SHIFT })),
|
|
268
|
+
);
|
|
269
|
+
expect(min.createBy.data.minutesBeforeShift).toBe(MIN_MINUTES_BEFORE_SHIFT);
|
|
270
|
+
|
|
271
|
+
const max: OnCreate<UserOnCallShiftReminder> =
|
|
272
|
+
await reminderHooks.onBeforeCreate(
|
|
273
|
+
userCreate(reminder({ minutesBeforeShift: MAX_MINUTES_BEFORE_SHIFT })),
|
|
274
|
+
);
|
|
275
|
+
expect(max.createBy.data.minutesBeforeShift).toBe(MAX_MINUTES_BEFORE_SHIFT);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("answers a duplicate lead with a message naming it, checked as root on the full key", async () => {
|
|
279
|
+
const countBy: jest.SpyInstance = getJestSpyOn(
|
|
280
|
+
UserOnCallShiftReminderService,
|
|
281
|
+
"countBy",
|
|
282
|
+
).mockResolvedValue(new PositiveNumber(1));
|
|
283
|
+
|
|
284
|
+
await expect(
|
|
285
|
+
reminderHooks.onBeforeCreate(
|
|
286
|
+
userCreate(reminder({ minutesBeforeShift: 60 })),
|
|
287
|
+
),
|
|
288
|
+
).rejects.toThrow(/already have a reminder 60 minutes/);
|
|
289
|
+
|
|
290
|
+
const call: {
|
|
291
|
+
query: Record<string, unknown>;
|
|
292
|
+
props: { isRoot?: boolean };
|
|
293
|
+
} = countBy.mock.calls[0]![0] as {
|
|
294
|
+
query: Record<string, unknown>;
|
|
295
|
+
props: { isRoot?: boolean };
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
expect(call.query).toEqual({
|
|
299
|
+
projectId: PROJECT_ID,
|
|
300
|
+
userId: USER_ID,
|
|
301
|
+
minutesBeforeShift: 60,
|
|
302
|
+
});
|
|
303
|
+
expect(call.props.isRoot).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("checks duplicates only after the lead has been validated (no NaN queries)", async () => {
|
|
307
|
+
const countBy: jest.SpyInstance = noDuplicates();
|
|
308
|
+
|
|
309
|
+
await expect(
|
|
310
|
+
reminderHooks.onBeforeCreate(
|
|
311
|
+
userCreate(reminder({ minutesBeforeShift: 3 })),
|
|
312
|
+
),
|
|
313
|
+
).rejects.toThrow(BadDataException);
|
|
314
|
+
|
|
315
|
+
expect(countBy).not.toHaveBeenCalled();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("UserOnCallShiftReminderService.onBeforeUpdate", () => {
|
|
320
|
+
test("validates and normalises the lead when present", async () => {
|
|
321
|
+
const onUpdate: OnUpdate<UserOnCallShiftReminder> =
|
|
322
|
+
await reminderHooks.onBeforeUpdate(
|
|
323
|
+
update<UserOnCallShiftReminder>({
|
|
324
|
+
minutesBeforeShift: "120" as unknown as number,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
expect(onUpdate.updateBy.data.minutesBeforeShift).toBe(120);
|
|
329
|
+
|
|
330
|
+
await expect(
|
|
331
|
+
reminderHooks.onBeforeUpdate(
|
|
332
|
+
update<UserOnCallShiftReminder>({ minutesBeforeShift: 5 }),
|
|
333
|
+
),
|
|
334
|
+
).rejects.toThrow(BadDataException);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("leaves an update without a lead alone", async () => {
|
|
338
|
+
const onUpdate: OnUpdate<UserOnCallShiftReminder> =
|
|
339
|
+
await reminderHooks.onBeforeUpdate(update<UserOnCallShiftReminder>({}));
|
|
340
|
+
|
|
341
|
+
expect(onUpdate.updateBy.data).toEqual({});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("UserOnCallShiftReminderService.validateMinutesBeforeShift", () => {
|
|
346
|
+
test("is the 15 ... 20160 whole-minute rule", () => {
|
|
347
|
+
expect(validateLead(15)).toBe(15);
|
|
348
|
+
expect(validateLead(20160)).toBe(20160);
|
|
349
|
+
expect(validateLead("10080")).toBe(10080);
|
|
350
|
+
|
|
351
|
+
for (const bad of [14, 20161, 1.5, "", "x", null, undefined, true, []]) {
|
|
352
|
+
expect(() => {
|
|
353
|
+
return validateLead(bad);
|
|
354
|
+
}).toThrow(BadDataException);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("UserOnCallShiftReminderLogService", () => {
|
|
360
|
+
function logRow(
|
|
361
|
+
overrides: Loose<UserOnCallShiftReminderLog> = {},
|
|
362
|
+
): UserOnCallShiftReminderLog {
|
|
363
|
+
const model: UserOnCallShiftReminderLog = new UserOnCallShiftReminderLog();
|
|
364
|
+
model.projectId = PROJECT_ID;
|
|
365
|
+
model.userId = USER_ID;
|
|
366
|
+
model.onCallDutyPolicyScheduleId = SCHEDULE_ID;
|
|
367
|
+
model.shiftStartsAt = new Date("2026-09-01T09:00:00.000Z");
|
|
368
|
+
model.minutesBeforeShift = 60;
|
|
369
|
+
model.kind = UserOnCallShiftReminderLogKind.Reminder;
|
|
370
|
+
Object.assign(model, overrides);
|
|
371
|
+
return model;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
test("refuses non-root writes in every direction", async () => {
|
|
375
|
+
await expect(logHooks.onBeforeCreate(userCreate(logRow()))).rejects.toThrow(
|
|
376
|
+
NotAuthorizedException,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
await expect(
|
|
380
|
+
logHooks.onBeforeUpdate(
|
|
381
|
+
update<UserOnCallShiftReminderLog>({ sentAt: new Date() }, false),
|
|
382
|
+
),
|
|
383
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
384
|
+
|
|
385
|
+
await expect(
|
|
386
|
+
logHooks.onBeforeDelete({
|
|
387
|
+
query: { _id: ROW_ID },
|
|
388
|
+
props: { isRoot: false },
|
|
389
|
+
limit: 1,
|
|
390
|
+
skip: 0,
|
|
391
|
+
} as unknown as DeleteBy<UserOnCallShiftReminderLog>),
|
|
392
|
+
).rejects.toThrow(NotAuthorizedException);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("lets root update and delete", async () => {
|
|
396
|
+
const onUpdate: OnUpdate<UserOnCallShiftReminderLog> =
|
|
397
|
+
await logHooks.onBeforeUpdate(
|
|
398
|
+
update<UserOnCallShiftReminderLog>({ sentAt: new Date() }, true),
|
|
399
|
+
);
|
|
400
|
+
expect(onUpdate.updateBy.data.sentAt).toBeInstanceOf(Date);
|
|
401
|
+
|
|
402
|
+
const onDelete: OnDelete<UserOnCallShiftReminderLog> =
|
|
403
|
+
await logHooks.onBeforeDelete({
|
|
404
|
+
query: { _id: ROW_ID },
|
|
405
|
+
props: { isRoot: true },
|
|
406
|
+
limit: 1,
|
|
407
|
+
skip: 0,
|
|
408
|
+
} as unknown as DeleteBy<UserOnCallShiftReminderLog>);
|
|
409
|
+
expect(onDelete.deleteBy.props.isRoot).toBe(true);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("accepts a complete claim row and stamps claimedAt now", async () => {
|
|
413
|
+
const before: number = Date.now();
|
|
414
|
+
|
|
415
|
+
const onCreate: OnCreate<UserOnCallShiftReminderLog> =
|
|
416
|
+
await logHooks.onBeforeCreate(rootCreate(logRow()));
|
|
417
|
+
|
|
418
|
+
expect(onCreate.createBy.data.claimedAt).toBeInstanceOf(Date);
|
|
419
|
+
expect(onCreate.createBy.data.claimedAt!.getTime()).toBeGreaterThanOrEqual(
|
|
420
|
+
before,
|
|
421
|
+
);
|
|
422
|
+
expect(onCreate.createBy.data.claimedAt!.getTime()).toBeLessThanOrEqual(
|
|
423
|
+
Date.now(),
|
|
424
|
+
);
|
|
425
|
+
expect(onCreate.createBy.data.sentAt).toBeUndefined();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("keeps a claimedAt the worker supplied", async () => {
|
|
429
|
+
const claimedAt: Date = new Date("2026-09-01T08:00:00.000Z");
|
|
430
|
+
|
|
431
|
+
const onCreate: OnCreate<UserOnCallShiftReminderLog> =
|
|
432
|
+
await logHooks.onBeforeCreate(rootCreate(logRow({ claimedAt })));
|
|
433
|
+
|
|
434
|
+
expect(onCreate.createBy.data.claimedAt).toBe(claimedAt);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("requires every part of the idempotency key", async () => {
|
|
438
|
+
for (const missing of [
|
|
439
|
+
"userId",
|
|
440
|
+
"onCallDutyPolicyScheduleId",
|
|
441
|
+
"shiftStartsAt",
|
|
442
|
+
"kind",
|
|
443
|
+
]) {
|
|
444
|
+
await expect(
|
|
445
|
+
logHooks.onBeforeCreate(rootCreate(logRow({ [missing]: undefined }))),
|
|
446
|
+
).rejects.toThrow(BadDataException);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
await expect(
|
|
450
|
+
logHooks.onBeforeCreate(
|
|
451
|
+
rootCreate(logRow({ projectId: undefined }), { tenantId: undefined }),
|
|
452
|
+
),
|
|
453
|
+
).rejects.toThrow(BadDataException);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("takes the project from the tenant when the row has none", async () => {
|
|
457
|
+
const onCreate: OnCreate<UserOnCallShiftReminderLog> =
|
|
458
|
+
await logHooks.onBeforeCreate(
|
|
459
|
+
rootCreate(logRow({ projectId: undefined }), {
|
|
460
|
+
tenantId: OTHER_PROJECT_ID,
|
|
461
|
+
}),
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
expect(onCreate.createBy.data.projectId).toBe(OTHER_PROJECT_ID);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("accepts exactly the three kinds and nothing else", async () => {
|
|
468
|
+
for (const kind of Object.values(UserOnCallShiftReminderLogKind)) {
|
|
469
|
+
await expect(
|
|
470
|
+
logHooks.onBeforeCreate(rootCreate(logRow({ kind }))),
|
|
471
|
+
).resolves.toBeDefined();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
for (const bad of ["Reminder", "nudge", "", undefined]) {
|
|
475
|
+
await expect(
|
|
476
|
+
logHooks.onBeforeCreate(
|
|
477
|
+
rootCreate(
|
|
478
|
+
logRow({ kind: bad as unknown as UserOnCallShiftReminderLogKind }),
|
|
479
|
+
),
|
|
480
|
+
),
|
|
481
|
+
).rejects.toThrow(BadDataException);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
test("truncates shiftStartsAt to the minute so a start that drifts by seconds collides on the unique key", async () => {
|
|
486
|
+
const onCreate: OnCreate<UserOnCallShiftReminderLog> =
|
|
487
|
+
await logHooks.onBeforeCreate(
|
|
488
|
+
rootCreate(
|
|
489
|
+
logRow({ shiftStartsAt: new Date("2026-09-01T09:00:59.999Z") }),
|
|
490
|
+
),
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
expect(onCreate.createBy.data.shiftStartsAt!.toISOString()).toBe(
|
|
494
|
+
"2026-09-01T09:00:00.000Z",
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const oneSecondLater: OnCreate<UserOnCallShiftReminderLog> =
|
|
498
|
+
await logHooks.onBeforeCreate(
|
|
499
|
+
rootCreate(
|
|
500
|
+
logRow({ shiftStartsAt: new Date("2026-09-01T09:00:01.000Z") }),
|
|
501
|
+
),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
expect(oneSecondLater.createBy.data.shiftStartsAt!.getTime()).toBe(
|
|
505
|
+
onCreate.createBy.data.shiftStartsAt!.getTime(),
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("accepts an ISO string for shiftStartsAt", async () => {
|
|
510
|
+
const onCreate: OnCreate<UserOnCallShiftReminderLog> =
|
|
511
|
+
await logHooks.onBeforeCreate(
|
|
512
|
+
rootCreate(
|
|
513
|
+
logRow({
|
|
514
|
+
shiftStartsAt: "2026-09-01T09:30:20.000Z" as unknown as Date,
|
|
515
|
+
}),
|
|
516
|
+
),
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
expect(onCreate.createBy.data.shiftStartsAt).toBeInstanceOf(Date);
|
|
520
|
+
expect(onCreate.createBy.data.shiftStartsAt!.toISOString()).toBe(
|
|
521
|
+
"2026-09-01T09:30:00.000Z",
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("defaults minutesBeforeShift to 0 for change notices and rejects junk", async () => {
|
|
526
|
+
const catchUp: OnCreate<UserOnCallShiftReminderLog> =
|
|
527
|
+
await logHooks.onBeforeCreate(
|
|
528
|
+
rootCreate(
|
|
529
|
+
logRow({
|
|
530
|
+
kind: UserOnCallShiftReminderLogKind.CatchUp,
|
|
531
|
+
minutesBeforeShift: undefined,
|
|
532
|
+
}),
|
|
533
|
+
),
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
expect(catchUp.createBy.data.minutesBeforeShift).toBe(0);
|
|
537
|
+
|
|
538
|
+
const asString: OnCreate<UserOnCallShiftReminderLog> =
|
|
539
|
+
await logHooks.onBeforeCreate(
|
|
540
|
+
rootCreate(logRow({ minutesBeforeShift: "15" as unknown as number })),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
expect(asString.createBy.data.minutesBeforeShift).toBe(15);
|
|
544
|
+
|
|
545
|
+
for (const bad of [-1, 2.5, "x"]) {
|
|
546
|
+
await expect(
|
|
547
|
+
logHooks.onBeforeCreate(
|
|
548
|
+
rootCreate(logRow({ minutesBeforeShift: bad as unknown as number })),
|
|
549
|
+
),
|
|
550
|
+
).rejects.toThrow(BadDataException);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("truncateToMinute zeroes seconds and milliseconds and is idempotent", () => {
|
|
555
|
+
const truncated: Date = logStatics.truncateToMinute(
|
|
556
|
+
new Date("2026-09-01T09:07:45.678Z"),
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
expect(truncated.toISOString()).toBe("2026-09-01T09:07:00.000Z");
|
|
560
|
+
expect(logStatics.truncateToMinute(truncated).getTime()).toBe(
|
|
561
|
+
truncated.getTime(),
|
|
562
|
+
);
|
|
563
|
+
expect(
|
|
564
|
+
logStatics
|
|
565
|
+
.truncateToMinute(new Date("2026-09-01T09:07:00.000Z"))
|
|
566
|
+
.toISOString(),
|
|
567
|
+
).toBe("2026-09-01T09:07:00.000Z");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("validateMinutesBeforeShift accepts 0 and whole positive minutes only", () => {
|
|
571
|
+
expect(logStatics.validateMinutesBeforeShift(undefined)).toBe(0);
|
|
572
|
+
expect(logStatics.validateMinutesBeforeShift(null)).toBe(0);
|
|
573
|
+
expect(logStatics.validateMinutesBeforeShift(0)).toBe(0);
|
|
574
|
+
expect(logStatics.validateMinutesBeforeShift(10080)).toBe(10080);
|
|
575
|
+
expect(logStatics.validateMinutesBeforeShift("60")).toBe(60);
|
|
576
|
+
|
|
577
|
+
for (const bad of [-1, 0.5, "", "abc", true]) {
|
|
578
|
+
expect(() => {
|
|
579
|
+
return logStatics.validateMinutesBeforeShift(bad);
|
|
580
|
+
}).toThrow(BadDataException);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
});
|