@oneuptime/common 11.7.0 → 11.7.2
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/DashboardDomain.ts +1 -0
- package/Models/DatabaseModels/Incident.ts +2 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
- package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
- package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
- package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
- package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
- package/Models/DatabaseModels/Monitor.ts +1 -0
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
- package/Models/DatabaseModels/NetworkSite.ts +1 -0
- package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
- package/Models/DatabaseModels/Probe.ts +13 -13
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
- package/Models/DatabaseModels/StatusPage.ts +1 -0
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
- package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
- package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
- package/Server/API/BaseAPI.ts +17 -1
- package/Server/API/CommonAPI.ts +32 -0
- package/Server/API/MicrosoftTeamsAPI.ts +99 -2
- package/Server/API/ProbeAPI.ts +2 -0
- package/Server/API/SlackAPI.ts +62 -0
- package/Server/API/StatusPageAPI.ts +157 -192
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/DatabaseService.ts +11 -3
- package/Server/Services/MonitorGroupService.ts +145 -0
- package/Server/Services/MonitorService.ts +231 -30
- package/Server/Services/ProbeService.ts +319 -16
- package/Server/Services/StatusPageService.ts +72 -42
- package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
- package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
- package/Server/Utils/Monitor/MonitorResource.ts +88 -35
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
- package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
- package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
- package/Tests/Server/API/ProbeAPI.test.ts +6 -0
- package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
- package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
- package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
- package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
- package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
- package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
- package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
- package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
- package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
- package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
- package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
- package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
- package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
- package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
- package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
- package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
- package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
- package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
- package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
- package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
- package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
- package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
- package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
- package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
- package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
- package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
- package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
- package/Tests/Types/DateUserTimezone.test.ts +205 -0
- package/Tests/Types/DateUserTimezoneOverridesBrowser.test.ts +73 -0
- package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
- package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
- package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
- package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
- package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
- package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
- package/Tests/UI/Components/TimePicker/TimePicker.test.tsx +11 -0
- package/Tests/UI/Utils/Project.test.ts +809 -0
- package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
- package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
- package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
- package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
- package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
- package/Types/Date.ts +119 -50
- package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
- package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
- package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
- package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
- package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
- package/UI/Components/Forms/Fields/FormField.tsx +13 -3
- package/UI/Components/Forms/ModelForm.tsx +41 -18
- package/UI/Components/Input/Input.tsx +6 -1
- package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
- package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +300 -30
- package/UI/Components/ModelTable/Column.ts +12 -0
- package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
- package/UI/Components/Navbar/NavBar.tsx +66 -1
- package/UI/Components/Table/Table.tsx +52 -13
- package/UI/Components/Table/Types/Column.ts +18 -0
- package/UI/Components/TimePicker/TimePicker.tsx +8 -4
- package/UI/Utils/Project.ts +214 -18
- package/UI/Utils/TableColumnsToCsv.ts +86 -6
- package/UI/Utils/User.ts +19 -0
- package/UI/esbuild-config.js +34 -0
- package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +2 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Probe.js +13 -13
- package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +12 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +21 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProbeAPI.js +2 -0
- package/build/dist/Server/API/ProbeAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +44 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +98 -152
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MonitorGroupService.js +118 -0
- package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +198 -24
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/ProbeService.js +238 -9
- package/build/dist/Server/Services/ProbeService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +46 -39
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Date.js +106 -35
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
- package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -3
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +6 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +234 -21
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
- package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
- package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +31 -11
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/TimePicker/TimePicker.js +4 -4
- package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -1
- package/build/dist/UI/Utils/Project.js +161 -15
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
- package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
- package/build/dist/UI/Utils/User.js +19 -0
- package/build/dist/UI/Utils/User.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
- package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
- package/package.json +4 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.ts +0 -79
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.ts +0 -41
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +0 -17
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js +0 -34
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js.map +0 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js +0 -22
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js.map +0 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +0 -12
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user's profile timezone (User Settings > Timezone) — not the zone the
|
|
3
|
+
* browser reports — decides which wall clock OneUptimeDate reads and writes.
|
|
4
|
+
*
|
|
5
|
+
* The bug these lock in: a user in America/New_York whose browser reported
|
|
6
|
+
* America/Adak was told "your local timezone - HDT" on every date field, and
|
|
7
|
+
* had to enter Hawaii-Aleutian times for their events to land correctly. The
|
|
8
|
+
* profile timezone was stored but never consulted by the UI.
|
|
9
|
+
*
|
|
10
|
+
* Every assertion is expressed in explicit-zone wall-clock (or absolute
|
|
11
|
+
* milliseconds) so it holds under whatever TZ the suite runs in.
|
|
12
|
+
*/
|
|
13
|
+
import OneUptimeDate from "../../Types/Date";
|
|
14
|
+
import Timezone from "../../Types/Timezone";
|
|
15
|
+
import moment from "moment-timezone";
|
|
16
|
+
|
|
17
|
+
const NY: Timezone = Timezone.AmericaNew_York;
|
|
18
|
+
const ADAK: Timezone = Timezone.AmericaAdak;
|
|
19
|
+
const KOLKATA: Timezone = Timezone.AsiaKolkata;
|
|
20
|
+
|
|
21
|
+
describe("OneUptimeDate user timezone", () => {
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
// Never leak an override into the rest of the suite.
|
|
24
|
+
OneUptimeDate.setUserTimezone(null);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("setUserTimezone / getCurrentTimezone", () => {
|
|
28
|
+
it("falls back to the browser / process zone when no user timezone is set", () => {
|
|
29
|
+
expect(OneUptimeDate.getUserTimezone()).toBeNull();
|
|
30
|
+
expect(OneUptimeDate.getCurrentTimezone().toString()).toBe(
|
|
31
|
+
moment.tz.guess(),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("prefers the user timezone over the browser / process zone", () => {
|
|
36
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
37
|
+
|
|
38
|
+
expect(OneUptimeDate.getCurrentTimezone()).toBe(NY);
|
|
39
|
+
expect(OneUptimeDate.getUserTimezone()).toBe(NY);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("reports the abbreviation of the user timezone, DST aware", () => {
|
|
43
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
44
|
+
|
|
45
|
+
// The bug reported "HDT" (America/Adak) for a New York user.
|
|
46
|
+
expect(OneUptimeDate.getCurrentTimezoneString()).not.toBe("HDT");
|
|
47
|
+
expect(["EST", "EDT"]).toContain(
|
|
48
|
+
OneUptimeDate.getCurrentTimezoneString(),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("clears back to the browser / process zone when set to null", () => {
|
|
53
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
54
|
+
OneUptimeDate.setUserTimezone(null);
|
|
55
|
+
|
|
56
|
+
expect(OneUptimeDate.getUserTimezone()).toBeNull();
|
|
57
|
+
expect(OneUptimeDate.getCurrentTimezone().toString()).toBe(
|
|
58
|
+
moment.tz.guess(),
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("ignores a value moment does not recognise as a zone rather than breaking every date", () => {
|
|
63
|
+
OneUptimeDate.setUserTimezone("Not/AZone" as Timezone);
|
|
64
|
+
|
|
65
|
+
expect(OneUptimeDate.getUserTimezone()).toBeNull();
|
|
66
|
+
expect(OneUptimeDate.getCurrentTimezone().toString()).toBe(
|
|
67
|
+
moment.tz.guess(),
|
|
68
|
+
);
|
|
69
|
+
// Formatting still works.
|
|
70
|
+
expect(
|
|
71
|
+
OneUptimeDate.toDateTimeLocalString(new Date("2026-07-09T13:00:00Z")),
|
|
72
|
+
).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("datetime-local round trip", () => {
|
|
77
|
+
it("renders a stored instant as its wall-clock in the user timezone", () => {
|
|
78
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
79
|
+
|
|
80
|
+
// 17:00 UTC on 2026-07-09 is 13:00 in New York (EDT).
|
|
81
|
+
expect(
|
|
82
|
+
OneUptimeDate.toDateTimeLocalString(new Date("2026-07-09T17:00:00Z")),
|
|
83
|
+
).toBe("2026-07-09T13:00:00");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("resolves a typed wall-clock in the user timezone, not the browser zone", () => {
|
|
87
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
88
|
+
|
|
89
|
+
const typed: Date =
|
|
90
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T13:00");
|
|
91
|
+
|
|
92
|
+
expect(typed.toISOString()).toBe("2026-07-09T17:00:00.000Z");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("round trips what the user typed back into the picker unchanged", () => {
|
|
96
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
97
|
+
|
|
98
|
+
const typed: Date =
|
|
99
|
+
OneUptimeDate.fromDateTimeLocalString("2026-01-15T09:30");
|
|
100
|
+
|
|
101
|
+
expect(OneUptimeDate.toDateTimeLocalString(typed)).toBe(
|
|
102
|
+
"2026-01-15T09:30:00",
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("stores different instants for the same wall-clock in different user timezones", () => {
|
|
107
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
108
|
+
const inNY: Date =
|
|
109
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T13:00");
|
|
110
|
+
|
|
111
|
+
OneUptimeDate.setUserTimezone(ADAK);
|
|
112
|
+
const inAdak: Date =
|
|
113
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T13:00");
|
|
114
|
+
|
|
115
|
+
// Adak is 5 hours behind New York in July.
|
|
116
|
+
expect(inAdak.getTime() - inNY.getTime()).toBe(5 * 60 * 60 * 1000);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("honours the user timezone's DST offset for a winter instant", () => {
|
|
120
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
121
|
+
|
|
122
|
+
// 09:00 EST is 14:00 UTC in January.
|
|
123
|
+
expect(
|
|
124
|
+
OneUptimeDate.fromDateTimeLocalString("2026-01-15T09:00").toISOString(),
|
|
125
|
+
).toBe("2026-01-15T14:00:00.000Z");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("display helpers", () => {
|
|
130
|
+
it("formats an instant in the user timezone with its abbreviation", () => {
|
|
131
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
132
|
+
|
|
133
|
+
const formatted: string = OneUptimeDate.getDateAsLocalFormattedString(
|
|
134
|
+
new Date("2026-07-09T17:00:00Z"),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(formatted).toContain("Jul 09 2026");
|
|
138
|
+
expect(formatted).toContain("13:00");
|
|
139
|
+
expect(formatted).toContain("EDT");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("reads the hour and minute of an instant in the user timezone", () => {
|
|
143
|
+
OneUptimeDate.setUserTimezone(KOLKATA);
|
|
144
|
+
|
|
145
|
+
const instant: Date = new Date("2026-07-09T17:00:00Z");
|
|
146
|
+
|
|
147
|
+
// 17:00 UTC is 22:30 in Kolkata (UTC+5:30).
|
|
148
|
+
expect(OneUptimeDate.getLocalHours(instant)).toBe(22);
|
|
149
|
+
expect(OneUptimeDate.getLocalMinutes(instant)).toBe(30);
|
|
150
|
+
expect(OneUptimeDate.getLocalTimeString(instant)).toBe("22:30");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("rolls the calendar day when the user timezone puts the instant on another date", () => {
|
|
154
|
+
OneUptimeDate.setUserTimezone(KOLKATA);
|
|
155
|
+
|
|
156
|
+
// 23:00 UTC on Jul 9 is already 04:30 on Jul 10 in Kolkata.
|
|
157
|
+
const instant: Date = new Date("2026-07-09T23:00:00Z");
|
|
158
|
+
|
|
159
|
+
expect(OneUptimeDate.asDateForDatabaseQuery(instant)).toBe("2026-07-10");
|
|
160
|
+
expect(OneUptimeDate.getDateAsLocalDayMonthString(instant)).toBe(
|
|
161
|
+
"10 Jul",
|
|
162
|
+
);
|
|
163
|
+
expect(OneUptimeDate.getDateAsLocalMonthYearString(instant)).toBe(
|
|
164
|
+
"Jul 2026",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("schedule-zone wall-clock bridge", () => {
|
|
170
|
+
it("reads the entered wall-clock in the user timezone before anchoring it to the schedule zone", () => {
|
|
171
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
172
|
+
|
|
173
|
+
// The admin typed 09:00 while working in New York.
|
|
174
|
+
const typed: Date =
|
|
175
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T09:00");
|
|
176
|
+
|
|
177
|
+
const stored: Date = OneUptimeDate.getInstantFromLocalWallClockInTimezone(
|
|
178
|
+
typed,
|
|
179
|
+
KOLKATA.toString(),
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
// ...and the schedule enforces 09:00 in Kolkata.
|
|
183
|
+
expect(moment.tz(stored, KOLKATA.toString()).format("HH:mm")).toBe(
|
|
184
|
+
"09:00",
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("redisplays a stored schedule-zone time as the same wall-clock in the picker", () => {
|
|
189
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
190
|
+
|
|
191
|
+
const stored: Date = moment
|
|
192
|
+
.tz("2026-07-09 09:00", KOLKATA.toString())
|
|
193
|
+
.toDate();
|
|
194
|
+
|
|
195
|
+
const forPicker: Date = OneUptimeDate.getLocalDateFromWallClockInTimezone(
|
|
196
|
+
stored,
|
|
197
|
+
KOLKATA.toString(),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(OneUptimeDate.toDateTimeLocalString(forPicker)).toBe(
|
|
201
|
+
"2026-07-09T09:00:00",
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** @timezone America/Adak */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regression test for the reported bug, reproduced exactly: the process (a
|
|
5
|
+
* stand-in for the customer's browser) reports America/Adak — abbreviated
|
|
6
|
+
* "HDT" — while the user has picked America/New_York in User Settings.
|
|
7
|
+
*
|
|
8
|
+
* Before the fix, every date field labelled itself "your local timezone - HDT"
|
|
9
|
+
* and resolved what the user typed in Adak's zone, so a New York user had to
|
|
10
|
+
* enter Hawaii-Aleutian times to get the right instant stored. The profile
|
|
11
|
+
* timezone must win over whatever the browser reports.
|
|
12
|
+
*/
|
|
13
|
+
import OneUptimeDate from "../../Types/Date";
|
|
14
|
+
import Timezone from "../../Types/Timezone";
|
|
15
|
+
import moment from "moment-timezone";
|
|
16
|
+
|
|
17
|
+
const NY: Timezone = Timezone.AmericaNew_York;
|
|
18
|
+
|
|
19
|
+
describe("user timezone overrides a browser reporting a different zone", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
OneUptimeDate.setUserTimezone(null);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("confirms the process really is on the Adak (HDT) clock", () => {
|
|
25
|
+
// Guards the premise of every assertion below.
|
|
26
|
+
expect(moment.tz.guess()).toBe("America/Adak");
|
|
27
|
+
expect(OneUptimeDate.getCurrentTimezoneString()).toBe("HDT");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("labels date fields with the profile timezone, not HDT", () => {
|
|
31
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
32
|
+
|
|
33
|
+
expect(OneUptimeDate.getCurrentTimezoneString()).toBe("EDT");
|
|
34
|
+
expect(OneUptimeDate.getCurrentTimezone()).toBe(NY);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("stores the wall-clock the user typed as New York time", () => {
|
|
38
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* The user schedules maintenance for 14:00 on Jul 9. That is 18:00 UTC in
|
|
42
|
+
* New York; reading it on the Adak clock would have stored 23:00 UTC —
|
|
43
|
+
* five hours off, which is exactly what the customer worked around.
|
|
44
|
+
*/
|
|
45
|
+
const startsAt: Date =
|
|
46
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T14:00");
|
|
47
|
+
|
|
48
|
+
expect(startsAt.toISOString()).toBe("2026-07-09T18:00:00.000Z");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("reads a stored event back at the wall-clock the user typed", () => {
|
|
52
|
+
OneUptimeDate.setUserTimezone(NY);
|
|
53
|
+
|
|
54
|
+
const startsAt: Date = new Date("2026-07-09T18:00:00.000Z");
|
|
55
|
+
|
|
56
|
+
expect(OneUptimeDate.toDateTimeLocalString(startsAt)).toBe(
|
|
57
|
+
"2026-07-09T14:00:00",
|
|
58
|
+
);
|
|
59
|
+
expect(OneUptimeDate.getDateAsLocalFormattedString(startsAt)).toContain(
|
|
60
|
+
"EDT",
|
|
61
|
+
);
|
|
62
|
+
expect(OneUptimeDate.getLocalTimeString(startsAt)).toBe("14:00");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("still follows the browser zone for a user who has not picked one", () => {
|
|
66
|
+
OneUptimeDate.setUserTimezone(null);
|
|
67
|
+
|
|
68
|
+
// 14:00 HDT (UTC-9) is 23:00 UTC the same day.
|
|
69
|
+
expect(
|
|
70
|
+
OneUptimeDate.fromDateTimeLocalString("2026-07-09T14:00").toISOString(),
|
|
71
|
+
).toBe("2026-07-09T23:00:00.000Z");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -98,7 +98,7 @@ describe("NotificationRuleConditionUtil", () => {
|
|
|
98
98
|
shouldPostToExistingChannel: false,
|
|
99
99
|
});
|
|
100
100
|
expect(validate(rule)).toBe(
|
|
101
|
-
"Please select
|
|
101
|
+
"Please select a destination: create a Slack channel or post to an existing Slack channel",
|
|
102
102
|
);
|
|
103
103
|
});
|
|
104
104
|
|
|
@@ -139,6 +139,224 @@ describe("NotificationRuleConditionUtil", () => {
|
|
|
139
139
|
});
|
|
140
140
|
});
|
|
141
141
|
|
|
142
|
+
describe("getValidationError - chats", () => {
|
|
143
|
+
const teamsChatEitherOrError: string =
|
|
144
|
+
"Please select a destination: create a Microsoft Teams channel, post to an existing Microsoft Teams channel, or post to an existing Microsoft Teams chat";
|
|
145
|
+
|
|
146
|
+
const teamsChatIdsError: string =
|
|
147
|
+
"Please select at least one Microsoft Teams chat to post to. If no chats are listed, add the OneUptime app to a chat in Microsoft Teams first.";
|
|
148
|
+
|
|
149
|
+
const validateTeams: (
|
|
150
|
+
rule: IncidentNotificationRule,
|
|
151
|
+
eventType?: NotificationRuleEventType,
|
|
152
|
+
) => string | null = (
|
|
153
|
+
rule: IncidentNotificationRule,
|
|
154
|
+
eventType: NotificationRuleEventType = NotificationRuleEventType.Incident,
|
|
155
|
+
): string | null => {
|
|
156
|
+
return NotificationRuleConditionUtil.getValidationError({
|
|
157
|
+
notificationRule: rule,
|
|
158
|
+
eventType: eventType,
|
|
159
|
+
workspaceType: WorkspaceType.MicrosoftTeams,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
test("a Teams rule posting only to an existing chat satisfies the either/or requirement", () => {
|
|
164
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
165
|
+
shouldCreateNewChannel: false,
|
|
166
|
+
shouldPostToExistingChannel: false,
|
|
167
|
+
shouldPostToExistingChat: true,
|
|
168
|
+
existingChatIds: ["c1"],
|
|
169
|
+
});
|
|
170
|
+
expect(validateTeams(rule)).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("a Teams rule with all three destination toggles off mentions the chat option in the error", () => {
|
|
174
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
175
|
+
shouldCreateNewChannel: false,
|
|
176
|
+
shouldPostToExistingChannel: false,
|
|
177
|
+
shouldPostToExistingChat: false,
|
|
178
|
+
});
|
|
179
|
+
expect(validateTeams(rule)).toBe(teamsChatEitherOrError);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("a Slack rule with both channel toggles off does not mention the chat option", () => {
|
|
183
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
184
|
+
shouldCreateNewChannel: false,
|
|
185
|
+
shouldPostToExistingChannel: false,
|
|
186
|
+
});
|
|
187
|
+
const error: string | null = validate(rule);
|
|
188
|
+
expect(error).toBe(
|
|
189
|
+
"Please select a destination: create a Slack channel or post to an existing Slack channel",
|
|
190
|
+
);
|
|
191
|
+
expect(error).not.toContain("chat");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("chat toggle on with an empty chat-id list returns the chat-selection error", () => {
|
|
195
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
196
|
+
shouldCreateNewChannel: false,
|
|
197
|
+
shouldPostToExistingChannel: false,
|
|
198
|
+
shouldPostToExistingChat: true,
|
|
199
|
+
existingChatIds: [],
|
|
200
|
+
});
|
|
201
|
+
expect(validateTeams(rule)).toBe(teamsChatIdsError);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("chat toggle on with undefined chat ids returns the chat-selection error", () => {
|
|
205
|
+
// existingChatIds is intentionally left off the rule so it is undefined.
|
|
206
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
207
|
+
shouldCreateNewChannel: false,
|
|
208
|
+
shouldPostToExistingChannel: false,
|
|
209
|
+
shouldPostToExistingChat: true,
|
|
210
|
+
});
|
|
211
|
+
expect(rule.existingChatIds).toBeUndefined();
|
|
212
|
+
expect(validateTeams(rule)).toBe(teamsChatIdsError);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("chat toggle on with chat ids passes even when channel toggles are also configured", () => {
|
|
216
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
217
|
+
shouldPostToExistingChat: true,
|
|
218
|
+
existingChatIds: ["19:abc@thread.v2", "19:def@unq.gbl.spaces"],
|
|
219
|
+
});
|
|
220
|
+
expect(validateTeams(rule)).toBeNull();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("chat-id validation still runs for the Monitor event type (either/or block is skipped)", () => {
|
|
224
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
225
|
+
shouldCreateNewChannel: false,
|
|
226
|
+
shouldPostToExistingChannel: false,
|
|
227
|
+
shouldPostToExistingChat: true,
|
|
228
|
+
existingChatIds: [],
|
|
229
|
+
});
|
|
230
|
+
expect(validateTeams(rule, NotificationRuleEventType.Monitor)).toBe(
|
|
231
|
+
teamsChatIdsError,
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("chat-id validation still runs for the OnCallDutyPolicy event type", () => {
|
|
236
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
237
|
+
shouldCreateNewChannel: false,
|
|
238
|
+
shouldPostToExistingChannel: false,
|
|
239
|
+
shouldPostToExistingChat: true,
|
|
240
|
+
existingChatIds: [],
|
|
241
|
+
});
|
|
242
|
+
expect(
|
|
243
|
+
validateTeams(rule, NotificationRuleEventType.OnCallDutyPolicy),
|
|
244
|
+
).toBe(teamsChatIdsError);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("a Monitor-event chat rule with chat ids passes", () => {
|
|
248
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
249
|
+
shouldCreateNewChannel: false,
|
|
250
|
+
shouldPostToExistingChannel: false,
|
|
251
|
+
shouldPostToExistingChat: true,
|
|
252
|
+
existingChatIds: ["c1"],
|
|
253
|
+
});
|
|
254
|
+
expect(validateTeams(rule, NotificationRuleEventType.Monitor)).toBeNull();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("a combined existing-channel + chat rule is valid", () => {
|
|
258
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
259
|
+
shouldCreateNewChannel: false,
|
|
260
|
+
shouldPostToExistingChannel: true,
|
|
261
|
+
existingChannelNames: "ops-alerts",
|
|
262
|
+
shouldPostToExistingChat: true,
|
|
263
|
+
existingChatIds: ["c1"],
|
|
264
|
+
});
|
|
265
|
+
expect(validateTeams(rule)).toBeNull();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("existing-channel toggle still requires channel names even when chat fields are valid", () => {
|
|
269
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
270
|
+
shouldCreateNewChannel: false,
|
|
271
|
+
shouldPostToExistingChannel: true,
|
|
272
|
+
existingChannelNames: " ",
|
|
273
|
+
shouldPostToExistingChat: true,
|
|
274
|
+
existingChatIds: ["c1"],
|
|
275
|
+
});
|
|
276
|
+
expect(validateTeams(rule)).toBe(
|
|
277
|
+
"Existing Microsoft Teams channel name is required",
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("create-channel toggle still requires a template name even when chat fields are valid", () => {
|
|
282
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
283
|
+
shouldCreateNewChannel: true,
|
|
284
|
+
newChannelTemplateName: " ",
|
|
285
|
+
shouldPostToExistingChannel: false,
|
|
286
|
+
shouldPostToExistingChat: true,
|
|
287
|
+
existingChatIds: ["c1"],
|
|
288
|
+
});
|
|
289
|
+
expect(validateTeams(rule)).toBe(
|
|
290
|
+
"New Microsoft Teams channel name is required",
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("channel-name errors take precedence over the chat-id error", () => {
|
|
295
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
296
|
+
shouldCreateNewChannel: false,
|
|
297
|
+
shouldPostToExistingChannel: true,
|
|
298
|
+
existingChannelNames: "",
|
|
299
|
+
shouldPostToExistingChat: true,
|
|
300
|
+
existingChatIds: [],
|
|
301
|
+
});
|
|
302
|
+
expect(validateTeams(rule)).toBe(
|
|
303
|
+
"Existing Microsoft Teams channel name is required",
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("chat-id validation is workspace-agnostic: a Slack rule with the chat toggle on and no ids errors too", () => {
|
|
308
|
+
/*
|
|
309
|
+
* The chat toggle is only surfaced in the Teams UI, but the validator
|
|
310
|
+
* does not gate the chat-ids check on workspace type. Pin the current
|
|
311
|
+
* behavior so a change here is deliberate.
|
|
312
|
+
*/
|
|
313
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
314
|
+
shouldPostToExistingChat: true,
|
|
315
|
+
existingChatIds: [],
|
|
316
|
+
});
|
|
317
|
+
expect(validate(rule)).toBe(
|
|
318
|
+
"Please select at least one Slack chat to post to. If no chats are listed, add the OneUptime app to a chat in Slack first.",
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test.each([
|
|
323
|
+
NotificationRuleEventType.Incident,
|
|
324
|
+
NotificationRuleEventType.Alert,
|
|
325
|
+
NotificationRuleEventType.AlertEpisode,
|
|
326
|
+
NotificationRuleEventType.IncidentEpisode,
|
|
327
|
+
NotificationRuleEventType.ScheduledMaintenance,
|
|
328
|
+
])(
|
|
329
|
+
"a chat-only Teams rule is accepted for the %s event type",
|
|
330
|
+
(eventType: NotificationRuleEventType) => {
|
|
331
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
332
|
+
shouldCreateNewChannel: false,
|
|
333
|
+
shouldPostToExistingChannel: false,
|
|
334
|
+
shouldPostToExistingChat: true,
|
|
335
|
+
existingChatIds: ["c1"],
|
|
336
|
+
});
|
|
337
|
+
expect(validateTeams(rule, eventType)).toBeNull();
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
test.each([
|
|
342
|
+
NotificationRuleEventType.Incident,
|
|
343
|
+
NotificationRuleEventType.Alert,
|
|
344
|
+
NotificationRuleEventType.AlertEpisode,
|
|
345
|
+
NotificationRuleEventType.IncidentEpisode,
|
|
346
|
+
NotificationRuleEventType.ScheduledMaintenance,
|
|
347
|
+
])(
|
|
348
|
+
"a Teams rule with no destination at all is rejected for the %s event type",
|
|
349
|
+
(eventType: NotificationRuleEventType) => {
|
|
350
|
+
const rule: IncidentNotificationRule = makeRule({
|
|
351
|
+
shouldCreateNewChannel: false,
|
|
352
|
+
shouldPostToExistingChannel: false,
|
|
353
|
+
shouldPostToExistingChat: false,
|
|
354
|
+
});
|
|
355
|
+
expect(validateTeams(rule, eventType)).toBe(teamsChatEitherOrError);
|
|
356
|
+
},
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
142
360
|
describe("hasValueField", () => {
|
|
143
361
|
test.each([
|
|
144
362
|
ConditionType.IsEmpty,
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import { fireEvent, render, RenderResult } from "@testing-library/react";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
5
|
+
import BulkUpdateForm, {
|
|
6
|
+
BulkActionButtonSchema,
|
|
7
|
+
ComponentProps,
|
|
8
|
+
} from "../../../UI/Components/BulkUpdate/BulkUpdateForm";
|
|
9
|
+
import { ButtonStyleType } from "../../../UI/Components/Button/Button";
|
|
10
|
+
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* react-i18next is not initialized in the test environment. Mock the hook so
|
|
14
|
+
* translate helpers echo their input and the component renders synchronously.
|
|
15
|
+
*/
|
|
16
|
+
jest.mock("react-i18next", () => {
|
|
17
|
+
return {
|
|
18
|
+
useTranslation: () => {
|
|
19
|
+
return {
|
|
20
|
+
t: (key: string, opts?: { defaultValue?: string }): string => {
|
|
21
|
+
return opts?.defaultValue ?? key;
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
interface Row {
|
|
29
|
+
_id?: string | undefined;
|
|
30
|
+
name?: string | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type MakeRowsFunction = (count: number) => Array<Row>;
|
|
34
|
+
|
|
35
|
+
const makeRows: MakeRowsFunction = (count: number): Array<Row> => {
|
|
36
|
+
const rows: Array<Row> = [];
|
|
37
|
+
|
|
38
|
+
for (let i: number = 0; i < count; i++) {
|
|
39
|
+
rows.push({ _id: `id-${i}`, name: `Row ${i}` });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return rows;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const BUTTONS: Array<BulkActionButtonSchema<Row>> = [
|
|
46
|
+
{
|
|
47
|
+
title: "Archive",
|
|
48
|
+
buttonStyleType: ButtonStyleType.NORMAL,
|
|
49
|
+
onClick: (): Promise<void> => {
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
type RenderFormFunction = (
|
|
56
|
+
overrides: Partial<ComponentProps<Row>>,
|
|
57
|
+
) => RenderResult;
|
|
58
|
+
|
|
59
|
+
const renderForm: RenderFormFunction = (
|
|
60
|
+
overrides: Partial<ComponentProps<Row>>,
|
|
61
|
+
): RenderResult => {
|
|
62
|
+
const props: ComponentProps<Row> = {
|
|
63
|
+
selectedItems: makeRows(3),
|
|
64
|
+
isAllItemsSelected: false,
|
|
65
|
+
onSelectAllClick: () => {},
|
|
66
|
+
onClearSelectionClick: () => {},
|
|
67
|
+
singularLabel: "Label",
|
|
68
|
+
pluralLabel: "Labels",
|
|
69
|
+
buttons: BUTTONS,
|
|
70
|
+
...overrides,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return render(<BulkUpdateForm<Row> {...props} />);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
describe("BulkUpdateForm", () => {
|
|
77
|
+
test("renders nothing when nothing is selected", () => {
|
|
78
|
+
const { container } = renderForm({ selectedItems: [] });
|
|
79
|
+
|
|
80
|
+
expect(container.innerHTML).toBe("");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("offers Select All while only part of the table is selected", () => {
|
|
84
|
+
const { getByText } = renderForm({ isAllItemsSelected: false });
|
|
85
|
+
|
|
86
|
+
expect(getByText("3 Labels Selected")).toBeTruthy();
|
|
87
|
+
expect(getByText("Select All Labels")).toBeTruthy();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("hides Select All once everything really is selected", () => {
|
|
91
|
+
const { queryByText } = renderForm({ isAllItemsSelected: true });
|
|
92
|
+
|
|
93
|
+
expect(queryByText("Select All Labels")).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("blocks a second Select All while the first is still loading", () => {
|
|
97
|
+
const onSelectAllClick: MockFunction = getJestMockFunction();
|
|
98
|
+
|
|
99
|
+
const { getByText } = renderForm({
|
|
100
|
+
isSelectingAllItems: true,
|
|
101
|
+
onSelectAllClick: onSelectAllClick as unknown as () => void,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const button: HTMLElement = getByText("Selecting All Labels...");
|
|
105
|
+
|
|
106
|
+
fireEvent.click(button);
|
|
107
|
+
|
|
108
|
+
/*
|
|
109
|
+
* Selecting everything is a paged, multi-request fetch. Letting it be
|
|
110
|
+
* re-entrant would interleave two runs over the same selection.
|
|
111
|
+
*/
|
|
112
|
+
expect(onSelectAllClick).not.toHaveBeenCalled();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("surfaces a failed select-all next to the button that triggered it", () => {
|
|
116
|
+
const { getByText } = renderForm({
|
|
117
|
+
errorMessage: "Network request failed",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(getByText("Network request failed")).toBeTruthy();
|
|
121
|
+
// The selection the user already had is still reported.
|
|
122
|
+
expect(getByText("3 Labels Selected")).toBeTruthy();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("shows no error when the select-all succeeded", () => {
|
|
126
|
+
const { queryByText } = renderForm({ errorMessage: undefined });
|
|
127
|
+
|
|
128
|
+
expect(queryByText(/failed/)).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("names real numbers when the selection was truncated", () => {
|
|
132
|
+
const { getByText } = renderForm({
|
|
133
|
+
selectedItems: makeRows(5),
|
|
134
|
+
isAllItemsSelected: true,
|
|
135
|
+
isSelectionTruncated: true,
|
|
136
|
+
totalMatchingItemsCount: 24318,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(
|
|
140
|
+
getByText(
|
|
141
|
+
new RegExp(`Selected 5 of ${(24318).toLocaleString()} matching Labels`),
|
|
142
|
+
),
|
|
143
|
+
).toBeTruthy();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("stays quiet when a full selection happens to sit on the ceiling", () => {
|
|
147
|
+
/*
|
|
148
|
+
* The warning used to be inferred from
|
|
149
|
+
* `selectedItems.length === LIMIT_PER_PROJECT`, so a project holding
|
|
150
|
+
* exactly the ceiling was told its selection had been cut short when
|
|
151
|
+
* nothing had been dropped at all.
|
|
152
|
+
*/
|
|
153
|
+
const { queryByText } = renderForm({
|
|
154
|
+
selectedItems: makeRows(LIMIT_PER_PROJECT),
|
|
155
|
+
isAllItemsSelected: true,
|
|
156
|
+
isSelectionTruncated: false,
|
|
157
|
+
totalMatchingItemsCount: LIMIT_PER_PROJECT,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
expect(queryByText(/matching Labels/)).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("clearing the selection calls back", () => {
|
|
164
|
+
const onClearSelectionClick: MockFunction = getJestMockFunction();
|
|
165
|
+
|
|
166
|
+
const { getByText } = renderForm({
|
|
167
|
+
onClearSelectionClick: onClearSelectionClick as unknown as () => void,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
fireEvent.click(getByText("Clear Selection"));
|
|
171
|
+
|
|
172
|
+
expect(onClearSelectionClick).toHaveBeenCalledTimes(1);
|
|
173
|
+
});
|
|
174
|
+
});
|