@oneuptime/common 14.0.5 → 14.0.7
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/InventoryItem.ts +23 -12
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.ts +217 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertService.ts +32 -36
- package/Server/Services/IncidentAlertService.ts +609 -2
- package/Server/Services/IncidentService.ts +134 -44
- package/Server/Services/InventoryItemService.ts +38 -0
- package/Server/Services/IoTDeviceCredentialService.ts +13 -4
- package/Server/Services/MetricService.ts +1 -1
- package/Server/Services/ScheduledMaintenanceService.ts +50 -46
- package/Server/Services/UserNotificationRuleService.ts +55 -53
- package/Server/Services/WorkspaceNotificationSummaryService.ts +38 -20
- package/Server/Types/Database/QueryHelper.ts +54 -1
- package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
- package/Server/Utils/AffectedResources/LinkedAffectedResources.ts +576 -0
- package/Server/Utils/Alert/AlertStateChangeAuthorization.ts +129 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
- package/Server/Utils/Inventory/InventoryOverviewAggregation.ts +111 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
- package/Server/Utils/Response.ts +31 -0
- package/Server/Utils/Topology/TopologyConcurrencyLimiter.ts +229 -0
- package/Server/Utils/Topology/TopologyQueries.ts +1350 -0
- package/Server/Utils/Topology/TopologyRequest.ts +463 -0
- package/Server/Utils/Topology/TopologyResponseCache.ts +183 -0
- package/Server/Utils/Topology/TopologySql.ts +1024 -0
- package/Tests/App/Dashboard/AcknowledgeAlertsOnDeclare.test.ts +369 -0
- package/Tests/App/Dashboard/AffectedResourcesSlo.test.tsx +1 -1
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +510 -123
- package/Tests/App/Dashboard/AlertsTableAffectedResources.test.tsx +490 -0
- package/Tests/App/Dashboard/ChangeAlertStateDeclareIncident.test.tsx +886 -0
- package/Tests/App/Dashboard/CreateReviewStepRoleSummary.test.tsx +1250 -0
- package/Tests/App/Dashboard/CreateReviewStepSelectionSummary.test.tsx +738 -0
- package/Tests/App/Dashboard/DeclareIncidentFromAlert.test.ts +417 -0
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +1948 -229
- package/Tests/App/Dashboard/EntityDetailPanelLifecycle.test.tsx +493 -0
- package/Tests/App/Dashboard/EntityDetailPanelNavigation.test.tsx +439 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +292 -1
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +798 -2
- package/Tests/App/Dashboard/FetchStateAndSeverityElements.test.tsx +669 -0
- package/Tests/App/Dashboard/FetchUsers.test.tsx +230 -0
- package/Tests/App/Dashboard/IncidentCreateAcknowledgeAlerts.test.tsx +1611 -0
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +1365 -57
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +964 -23
- package/Tests/App/Dashboard/InventoryRelationships.test.tsx +885 -0
- package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +201 -15
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +508 -36
- package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
- package/Tests/App/Dashboard/TopologyDataLifecycle.test.tsx +583 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +982 -218
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +818 -114
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +90 -21
- package/Tests/Server/Services/DomainService.test.ts +525 -0
- package/Tests/Server/Services/IncidentAlertAcknowledgeOnDeclare.test.ts +2630 -0
- package/Tests/Server/Services/IncidentAlertPostgres.test.ts +1625 -1
- package/Tests/Server/Services/IncidentDeclareAcknowledgeAlerts.test.ts +2189 -0
- package/Tests/Server/Services/InventoryItemServiceOverviewCounts.test.ts +345 -0
- package/Tests/Server/Services/InventoryOverviewPostgres.test.ts +254 -0
- package/Tests/Server/Services/IoTDeviceCredentialService.test.ts +819 -0
- package/Tests/Server/Services/LinkedAffectedResourcesCreatedFeed.test.ts +420 -0
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
- package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
- package/Tests/Server/Services/SloAffectedResourceCreatedFeed.test.ts +92 -36
- package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
- package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
- package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
- package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
- package/Tests/Server/Utils/AffectedResources/LinkedAffectedResources.test.ts +858 -0
- package/Tests/Server/Utils/Alert/AlertStateChangeAuthorization.test.ts +1172 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
- package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigration.test.ts +386 -0
- package/Tests/Server/Utils/Topology/InventoryItemEntityKeyIndexMigrationPostgres.test.ts +430 -0
- package/Tests/Server/Utils/Topology/TopologyActivityParityPostgres.test.ts +279 -0
- package/Tests/Server/Utils/Topology/TopologyConcurrencyLimiter.test.ts +534 -0
- package/Tests/Server/Utils/Topology/TopologyEstateGenerator.ts +624 -0
- package/Tests/Server/Utils/Topology/TopologyJsonResponse.test.ts +90 -0
- package/Tests/Server/Utils/Topology/TopologyQueries.test.ts +1634 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesDifferentialPostgres.test.ts +683 -0
- package/Tests/Server/Utils/Topology/TopologyQueriesPostgres.test.ts +2624 -0
- package/Tests/Server/Utils/Topology/TopologyReference.ts +1018 -0
- package/Tests/Server/Utils/Topology/TopologyRequest.test.ts +643 -0
- package/Tests/Server/Utils/Topology/TopologyResponseCache.test.ts +305 -0
- package/Tests/Server/Utils/Topology/TopologySql.test.ts +973 -0
- package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
- package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
- package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
- package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
- package/Tests/Types/Topology/TopologyTypeRules.test.ts +176 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
- package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
- package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +302 -0
- package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
- package/Types/BaseDatabase/AggregationType.ts +7 -0
- package/Types/Dashboard/DashboardTemplates.ts +20 -7
- package/Types/Incident/IncidentAlertLink.ts +19 -0
- package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
- package/Types/Rum/WebVitals.ts +13 -0
- package/Types/Telemetry/InventoryLiveness.ts +26 -0
- package/Types/Topology/TopologyApi.ts +428 -0
- package/Types/Topology/TopologyTypeRules.ts +191 -0
- package/UI/Components/Input/Input.tsx +11 -0
- package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
- package/Utils/Incident/IncidentFromAlerts.ts +78 -0
- package/build/dist/Models/DatabaseModels/InventoryItem.js +20 -11
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.js +165 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795200000000-AddInventoryItemProjectEntityKeyIndex.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/AlertService.js +21 -33
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentAlertService.js +457 -3
- package/build/dist/Server/Services/IncidentAlertService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +96 -42
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +31 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceCredentialService.js +12 -5
- package/build/dist/Server/Services/IoTDeviceCredentialService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceService.js +37 -40
- package/build/dist/Server/Services/ScheduledMaintenanceService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +55 -51
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js +42 -25
- package/build/dist/Server/Services/WorkspaceNotificationSummaryService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js +405 -0
- package/build/dist/Server/Utils/AffectedResources/LinkedAffectedResources.js.map +1 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js +112 -0
- package/build/dist/Server/Utils/Alert/AlertStateChangeAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js +81 -0
- package/build/dist/Server/Utils/Inventory/InventoryOverviewAggregation.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +30 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js +178 -0
- package/build/dist/Server/Utils/Topology/TopologyConcurrencyLimiter.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js +884 -0
- package/build/dist/Server/Utils/Topology/TopologyQueries.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js +275 -0
- package/build/dist/Server/Utils/Topology/TopologyRequest.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js +146 -0
- package/build/dist/Server/Utils/Topology/TopologyResponseCache.js.map +1 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js +688 -0
- package/build/dist/Server/Utils/Topology/TopologySql.js.map +1 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Incident/IncidentAlertLink.js +17 -0
- package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +12 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/Types/Telemetry/InventoryLiveness.js +23 -0
- package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -1
- package/build/dist/Types/Topology/TopologyApi.js +55 -0
- package/build/dist/Types/Topology/TopologyApi.js.map +1 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js +165 -0
- package/build/dist/Types/Topology/TopologyTypeRules.js.map +1 -0
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js +38 -0
- package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import LogSavedViewService from "../../../Server/Services/LogSavedViewService";
|
|
2
|
+
import MetricSavedViewService from "../../../Server/Services/MetricSavedViewService";
|
|
3
|
+
import TraceSavedViewService from "../../../Server/Services/TraceSavedViewService";
|
|
4
|
+
import CreateBy from "../../../Server/Types/Database/CreateBy";
|
|
5
|
+
import UpdateBy from "../../../Server/Types/Database/UpdateBy";
|
|
6
|
+
import { OnCreate, OnUpdate } from "../../../Server/Types/Database/Hooks";
|
|
7
|
+
import LogSavedView from "../../../Models/DatabaseModels/LogSavedView";
|
|
8
|
+
import MetricSavedView from "../../../Models/DatabaseModels/MetricSavedView";
|
|
9
|
+
import TraceSavedView from "../../../Models/DatabaseModels/TraceSavedView";
|
|
10
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
11
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
12
|
+
import { afterEach, describe, expect, test } from "@jest/globals";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Telemetry saved views (logs, traces and metrics) each promise a project
|
|
16
|
+
* exactly one default: the first view a project saves becomes its default, and
|
|
17
|
+
* marking another one default clears whichever view held it. Three services
|
|
18
|
+
* carry the same hooks, so the suite runs against all three -- a change made
|
|
19
|
+
* to one of them and not the others shows up here rather than as a project
|
|
20
|
+
* whose telemetry page opens two default views, or none.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
type SavedViewModel = LogSavedView | MetricSavedView | TraceSavedView;
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* The two hooks under test, reached past `protected`. Typed over the union of
|
|
27
|
+
* the three models so one set of cases can drive all three services.
|
|
28
|
+
*/
|
|
29
|
+
type SavedViewService = {
|
|
30
|
+
onBeforeCreate: (
|
|
31
|
+
createBy: CreateBy<SavedViewModel>,
|
|
32
|
+
) => Promise<OnCreate<SavedViewModel>>;
|
|
33
|
+
onBeforeUpdate: (
|
|
34
|
+
updateBy: UpdateBy<SavedViewModel>,
|
|
35
|
+
) => Promise<OnUpdate<SavedViewModel>>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type ServiceCase = {
|
|
39
|
+
label: string;
|
|
40
|
+
service: SavedViewService;
|
|
41
|
+
buildModel: () => SavedViewModel;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
45
|
+
"11111111-1111-4111-8111-111111111111",
|
|
46
|
+
);
|
|
47
|
+
const VIEW_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
|
|
48
|
+
|
|
49
|
+
const cases: Array<ServiceCase> = [
|
|
50
|
+
{
|
|
51
|
+
label: "LogSavedViewService",
|
|
52
|
+
service: LogSavedViewService as unknown as SavedViewService,
|
|
53
|
+
buildModel: (): SavedViewModel => {
|
|
54
|
+
return new LogSavedView();
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "TraceSavedViewService",
|
|
59
|
+
service: TraceSavedViewService as unknown as SavedViewService,
|
|
60
|
+
buildModel: (): SavedViewModel => {
|
|
61
|
+
return new TraceSavedView();
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "MetricSavedViewService",
|
|
66
|
+
service: MetricSavedViewService as unknown as SavedViewService,
|
|
67
|
+
buildModel: (): SavedViewModel => {
|
|
68
|
+
return new MetricSavedView();
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
jest.restoreAllMocks();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe.each(cases)(
|
|
78
|
+
"$label default-view hooks",
|
|
79
|
+
({ service, buildModel }: ServiceCase) => {
|
|
80
|
+
function mockFindOneBy(existing: SavedViewModel | null): jest.SpyInstance {
|
|
81
|
+
return jest
|
|
82
|
+
.spyOn(service as never, "findOneBy")
|
|
83
|
+
.mockResolvedValue(existing as never);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mockFindBy(items: Array<SavedViewModel>): jest.SpyInstance {
|
|
87
|
+
return jest
|
|
88
|
+
.spyOn(service as never, "findBy")
|
|
89
|
+
.mockResolvedValue(items as never);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mockUpdateBy(): jest.SpyInstance {
|
|
93
|
+
return jest
|
|
94
|
+
.spyOn(service as never, "updateBy")
|
|
95
|
+
.mockResolvedValue(undefined as never);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
describe("onBeforeCreate", () => {
|
|
99
|
+
test("the first view a project saves becomes its default", async () => {
|
|
100
|
+
mockFindOneBy(null);
|
|
101
|
+
const updateSpy: jest.SpyInstance = mockUpdateBy();
|
|
102
|
+
|
|
103
|
+
const view: SavedViewModel = buildModel();
|
|
104
|
+
view.projectId = PROJECT_ID;
|
|
105
|
+
|
|
106
|
+
const result: OnCreate<SavedViewModel> = await service.onBeforeCreate({
|
|
107
|
+
data: view,
|
|
108
|
+
} as CreateBy<SavedViewModel>);
|
|
109
|
+
|
|
110
|
+
expect(result.createBy.data.isDefault).toBe(true);
|
|
111
|
+
/*
|
|
112
|
+
* Nothing else held the default, but the sweep still runs and is a no-op
|
|
113
|
+
* against an empty project.
|
|
114
|
+
*/
|
|
115
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a later view is not made default while one already exists", async () => {
|
|
119
|
+
const existing: SavedViewModel = buildModel();
|
|
120
|
+
existing._id = VIEW_ID.toString();
|
|
121
|
+
mockFindOneBy(existing);
|
|
122
|
+
const updateSpy: jest.SpyInstance = mockUpdateBy();
|
|
123
|
+
|
|
124
|
+
const view: SavedViewModel = buildModel();
|
|
125
|
+
view.projectId = PROJECT_ID;
|
|
126
|
+
|
|
127
|
+
const result: OnCreate<SavedViewModel> = await service.onBeforeCreate({
|
|
128
|
+
data: view,
|
|
129
|
+
} as CreateBy<SavedViewModel>);
|
|
130
|
+
|
|
131
|
+
expect(result.createBy.data.isDefault).toBe(false);
|
|
132
|
+
// Not default, so nothing is unset.
|
|
133
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("an explicit isDefault=true clears whichever view held it", async () => {
|
|
137
|
+
const findOneSpy: jest.SpyInstance = mockFindOneBy(null);
|
|
138
|
+
const updateSpy: jest.SpyInstance = mockUpdateBy();
|
|
139
|
+
|
|
140
|
+
const view: SavedViewModel = buildModel();
|
|
141
|
+
view.projectId = PROJECT_ID;
|
|
142
|
+
view.isDefault = true;
|
|
143
|
+
|
|
144
|
+
await service.onBeforeCreate({
|
|
145
|
+
data: view,
|
|
146
|
+
} as CreateBy<SavedViewModel>);
|
|
147
|
+
|
|
148
|
+
// The caller said so, so the service does not go looking.
|
|
149
|
+
expect(findOneSpy).not.toHaveBeenCalled();
|
|
150
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
151
|
+
const updateArgs: Record<string, unknown> = updateSpy.mock
|
|
152
|
+
.calls[0]?.[0] as Record<string, unknown>;
|
|
153
|
+
const query: Record<string, unknown> = updateArgs["query"] as Record<
|
|
154
|
+
string,
|
|
155
|
+
unknown
|
|
156
|
+
>;
|
|
157
|
+
expect(query["projectId"]).toBe(PROJECT_ID);
|
|
158
|
+
expect(query["isDefault"]).toBe(true);
|
|
159
|
+
expect(updateArgs["data"]).toEqual({ isDefault: false });
|
|
160
|
+
/*
|
|
161
|
+
* The sweep runs as root: a member may only see their own views, and
|
|
162
|
+
* leaving the others default would break the one-default promise.
|
|
163
|
+
*/
|
|
164
|
+
expect(updateArgs["props"]).toEqual({ isRoot: true });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("an explicit isDefault=false is kept, and unsets nothing", async () => {
|
|
168
|
+
const findOneSpy: jest.SpyInstance = mockFindOneBy(null);
|
|
169
|
+
const updateSpy: jest.SpyInstance = mockUpdateBy();
|
|
170
|
+
|
|
171
|
+
const view: SavedViewModel = buildModel();
|
|
172
|
+
view.projectId = PROJECT_ID;
|
|
173
|
+
view.isDefault = false;
|
|
174
|
+
|
|
175
|
+
const result: OnCreate<SavedViewModel> = await service.onBeforeCreate({
|
|
176
|
+
data: view,
|
|
177
|
+
} as CreateBy<SavedViewModel>);
|
|
178
|
+
|
|
179
|
+
expect(result.createBy.data.isDefault).toBe(false);
|
|
180
|
+
expect(findOneSpy).not.toHaveBeenCalled();
|
|
181
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("a view with no project is left alone", async () => {
|
|
185
|
+
const findOneSpy: jest.SpyInstance = mockFindOneBy(null);
|
|
186
|
+
const updateSpy: jest.SpyInstance = mockUpdateBy();
|
|
187
|
+
|
|
188
|
+
const view: SavedViewModel = buildModel();
|
|
189
|
+
|
|
190
|
+
const result: OnCreate<SavedViewModel> = await service.onBeforeCreate({
|
|
191
|
+
data: view,
|
|
192
|
+
} as CreateBy<SavedViewModel>);
|
|
193
|
+
|
|
194
|
+
expect(result.createBy.data.isDefault).toBeUndefined();
|
|
195
|
+
expect(findOneSpy).not.toHaveBeenCalled();
|
|
196
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("the lookup for an existing default is scoped to the same project", async () => {
|
|
200
|
+
const findOneSpy: jest.SpyInstance = mockFindOneBy(null);
|
|
201
|
+
mockUpdateBy();
|
|
202
|
+
|
|
203
|
+
const view: SavedViewModel = buildModel();
|
|
204
|
+
view.projectId = PROJECT_ID;
|
|
205
|
+
|
|
206
|
+
await service.onBeforeCreate({
|
|
207
|
+
data: view,
|
|
208
|
+
} as CreateBy<SavedViewModel>);
|
|
209
|
+
|
|
210
|
+
const findArgs: Record<string, unknown> = findOneSpy.mock
|
|
211
|
+
.calls[0]?.[0] as Record<string, unknown>;
|
|
212
|
+
expect(findArgs["query"]).toEqual({
|
|
213
|
+
projectId: PROJECT_ID,
|
|
214
|
+
isDefault: true,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("onBeforeUpdate", () => {
|
|
220
|
+
test("promoting a view to default demotes the others, but not itself", async () => {
|
|
221
|
+
const item: SavedViewModel = buildModel();
|
|
222
|
+
item._id = VIEW_ID.toString();
|
|
223
|
+
item.projectId = PROJECT_ID;
|
|
224
|
+
mockFindBy([item]);
|
|
225
|
+
const updateSpy: jest.SpyInstance = mockUpdateBy();
|
|
226
|
+
|
|
227
|
+
await service.onBeforeUpdate({
|
|
228
|
+
query: { _id: VIEW_ID.toString() },
|
|
229
|
+
data: { isDefault: true },
|
|
230
|
+
} as unknown as UpdateBy<SavedViewModel>);
|
|
231
|
+
|
|
232
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
233
|
+
const query: Record<string, unknown> = (
|
|
234
|
+
updateSpy.mock.calls[0]?.[0] as Record<string, unknown>
|
|
235
|
+
)["query"] as Record<string, unknown>;
|
|
236
|
+
expect(query["projectId"]).toBe(PROJECT_ID);
|
|
237
|
+
expect(query["isDefault"]).toBe(true);
|
|
238
|
+
/*
|
|
239
|
+
* The view being promoted is excluded, so the same statement cannot
|
|
240
|
+
* demote it again.
|
|
241
|
+
*/
|
|
242
|
+
expect(query["_id"]).toBeDefined();
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("an update that does not set isDefault=true touches nothing", async () => {
|
|
246
|
+
const findSpy: jest.SpyInstance = mockFindBy([]);
|
|
247
|
+
const updateSpy: jest.SpyInstance = mockUpdateBy();
|
|
248
|
+
|
|
249
|
+
for (const data of [{ name: "Renamed" }, { isDefault: false }] as Array<
|
|
250
|
+
Partial<SavedViewModel>
|
|
251
|
+
>) {
|
|
252
|
+
await service.onBeforeUpdate({
|
|
253
|
+
query: { _id: VIEW_ID.toString() },
|
|
254
|
+
data: data,
|
|
255
|
+
} as unknown as UpdateBy<SavedViewModel>);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
expect(findSpy).not.toHaveBeenCalled();
|
|
259
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("a bulk promotion demotes the others in each affected project", async () => {
|
|
263
|
+
const otherProjectId: ObjectID = new ObjectID(
|
|
264
|
+
"33333333-3333-4333-8333-333333333333",
|
|
265
|
+
);
|
|
266
|
+
const first: SavedViewModel = buildModel();
|
|
267
|
+
first._id = VIEW_ID.toString();
|
|
268
|
+
first.projectId = PROJECT_ID;
|
|
269
|
+
const second: SavedViewModel = buildModel();
|
|
270
|
+
second._id = "44444444-4444-4444-8444-444444444444";
|
|
271
|
+
second.projectId = otherProjectId;
|
|
272
|
+
mockFindBy([first, second]);
|
|
273
|
+
const updateSpy: jest.SpyInstance = mockUpdateBy();
|
|
274
|
+
|
|
275
|
+
await service.onBeforeUpdate({
|
|
276
|
+
query: { isDefault: false },
|
|
277
|
+
data: { isDefault: true },
|
|
278
|
+
} as unknown as UpdateBy<SavedViewModel>);
|
|
279
|
+
|
|
280
|
+
expect(updateSpy).toHaveBeenCalledTimes(2);
|
|
281
|
+
const projectIds: Array<unknown> = updateSpy.mock.calls.map(
|
|
282
|
+
(call: Array<unknown>) => {
|
|
283
|
+
return (
|
|
284
|
+
(call[0] as Record<string, unknown>)["query"] as Record<
|
|
285
|
+
string,
|
|
286
|
+
unknown
|
|
287
|
+
>
|
|
288
|
+
)["projectId"];
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
expect(projectIds).toEqual([PROJECT_ID, otherProjectId]);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("a matched view with no project is skipped rather than swept globally", async () => {
|
|
295
|
+
const orphan: SavedViewModel = buildModel();
|
|
296
|
+
orphan._id = VIEW_ID.toString();
|
|
297
|
+
mockFindBy([orphan]);
|
|
298
|
+
const updateSpy: jest.SpyInstance = mockUpdateBy();
|
|
299
|
+
|
|
300
|
+
await service.onBeforeUpdate({
|
|
301
|
+
query: { _id: VIEW_ID.toString() },
|
|
302
|
+
data: { isDefault: true },
|
|
303
|
+
} as unknown as UpdateBy<SavedViewModel>);
|
|
304
|
+
|
|
305
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
describe("the three saved-view services", () => {
|
|
312
|
+
test("are distinct services over distinct models", () => {
|
|
313
|
+
const models: Array<string> = cases.map(({ buildModel }: ServiceCase) => {
|
|
314
|
+
return (buildModel() as BaseModel).tableName as string;
|
|
315
|
+
});
|
|
316
|
+
expect(new Set(models).size).toBe(3);
|
|
317
|
+
expect(
|
|
318
|
+
new Set([
|
|
319
|
+
LogSavedViewService,
|
|
320
|
+
TraceSavedViewService,
|
|
321
|
+
MetricSavedViewService,
|
|
322
|
+
]).size,
|
|
323
|
+
).toBe(3);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -3,7 +3,6 @@ import AlertFeedService from "../../../Server/Services/AlertFeedService";
|
|
|
3
3
|
import AlertService from "../../../Server/Services/AlertService";
|
|
4
4
|
import IncidentFeedService from "../../../Server/Services/IncidentFeedService";
|
|
5
5
|
import IncidentService from "../../../Server/Services/IncidentService";
|
|
6
|
-
import MonitorService from "../../../Server/Services/MonitorService";
|
|
7
6
|
import ProductAnalytics from "../../../Server/Utils/ProductAnalytics";
|
|
8
7
|
import AlertWorkspaceMessages from "../../../Server/Utils/Workspace/WorkspaceMessages/Alert";
|
|
9
8
|
import IncidentWorkspaceMessages from "../../../Server/Utils/Workspace/WorkspaceMessages/Incident";
|
|
@@ -26,6 +25,11 @@ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
|
26
25
|
*
|
|
27
26
|
* The same markdown is posted to Slack and Teams, and the feed renders it
|
|
28
27
|
* without safe mode, so a hostile SLO name is checked end to end here too.
|
|
28
|
+
*
|
|
29
|
+
* The feed builders read those relations themselves, one per query, through
|
|
30
|
+
* LinkedAffectedResources; the fixtures below answer each of those reads the
|
|
31
|
+
* way the database would. Every other relation the section now lists is
|
|
32
|
+
* covered in LinkedAffectedResourcesCreatedFeed.test.ts.
|
|
29
33
|
*/
|
|
30
34
|
|
|
31
35
|
const DASHBOARD: string = "https://oneuptime.example/dashboard";
|
|
@@ -52,8 +56,40 @@ type OnCreateSuccessFunction = (
|
|
|
52
56
|
createdItem: Incident,
|
|
53
57
|
) => Promise<Incident>;
|
|
54
58
|
|
|
59
|
+
/*
|
|
60
|
+
* Answers the feed builder's per-relation reads from `record`: one row
|
|
61
|
+
* carrying only the relation that was selected, as the database would.
|
|
62
|
+
*/
|
|
63
|
+
function answerRelationReads(
|
|
64
|
+
service: { findAllBy: unknown },
|
|
65
|
+
record: Incident | Alert,
|
|
66
|
+
): jest.SpyInstance {
|
|
67
|
+
return jest
|
|
68
|
+
.spyOn(service as typeof IncidentService, "findAllBy")
|
|
69
|
+
.mockImplementation((async (findAllBy: {
|
|
70
|
+
select: JSONObject;
|
|
71
|
+
}): Promise<Array<JSONObject>> => {
|
|
72
|
+
const row: JSONObject = {
|
|
73
|
+
_id: record._id!,
|
|
74
|
+
projectId: record.projectId as unknown as JSONObject,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (const column of Object.keys(findAllBy.select)) {
|
|
78
|
+
if (column !== "_id" && column !== "projectId") {
|
|
79
|
+
row[column] = (record as unknown as JSONObject)[column]!;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [row];
|
|
84
|
+
}) as never);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let incidentRelationReads: jest.SpyInstance | undefined;
|
|
88
|
+
|
|
55
89
|
// The feed writers are private; they are reached the way the create hooks reach them.
|
|
56
90
|
function createIncidentFeed(incident: Incident): Promise<void> {
|
|
91
|
+
incidentRelationReads = answerRelationReads(IncidentService, incident);
|
|
92
|
+
|
|
57
93
|
return (
|
|
58
94
|
IncidentService as unknown as {
|
|
59
95
|
createIncidentFeedAsync: CreateIncidentFeedAsyncFunction;
|
|
@@ -97,6 +133,8 @@ function buildMonitor(id: string, name: string): Monitor {
|
|
|
97
133
|
const monitor: Monitor = new Monitor();
|
|
98
134
|
monitor._id = id;
|
|
99
135
|
monitor.name = name;
|
|
136
|
+
// The relation reads select it; a monitor of no known project is not named.
|
|
137
|
+
monitor.projectId = PROJECT_ID;
|
|
100
138
|
return monitor;
|
|
101
139
|
}
|
|
102
140
|
|
|
@@ -163,7 +201,6 @@ function resourcesAffectedLines(markdown: string): Array<string> | null {
|
|
|
163
201
|
|
|
164
202
|
let incidentFeedItem: jest.SpyInstance;
|
|
165
203
|
let alertFeedItem: jest.SpyInstance;
|
|
166
|
-
let dashboardUrlLookup: jest.SpyInstance;
|
|
167
204
|
|
|
168
205
|
function postedMarkdown(spy: jest.SpyInstance): string {
|
|
169
206
|
expect(spy).toHaveBeenCalledTimes(1);
|
|
@@ -177,22 +214,12 @@ beforeEach(() => {
|
|
|
177
214
|
* A fresh URL per call: URL.addRoute mutates, so a shared instance would
|
|
178
215
|
* hide a builder that forgot to copy it.
|
|
179
216
|
*/
|
|
180
|
-
|
|
217
|
+
jest
|
|
181
218
|
.spyOn(DatabaseConfig, "getDashboardUrl")
|
|
182
219
|
.mockImplementation(async (): Promise<URL> => {
|
|
183
220
|
return URL.fromString(DASHBOARD);
|
|
184
221
|
});
|
|
185
222
|
|
|
186
|
-
jest
|
|
187
|
-
.spyOn(MonitorService, "getMonitorLinkInDashboard")
|
|
188
|
-
.mockImplementation(
|
|
189
|
-
async (projectId: ObjectID, monitorId: ObjectID): Promise<URL> => {
|
|
190
|
-
return URL.fromString(
|
|
191
|
-
`${DASHBOARD}/${projectId.toString()}/monitors/${monitorId.toString()}`,
|
|
192
|
-
);
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
|
|
196
223
|
jest
|
|
197
224
|
.spyOn(IncidentWorkspaceMessages, "getIncidentCreateMessageBlocks")
|
|
198
225
|
.mockResolvedValue([] as never);
|
|
@@ -246,7 +273,7 @@ describe("incident created feed item", () => {
|
|
|
246
273
|
]);
|
|
247
274
|
});
|
|
248
275
|
|
|
249
|
-
test("a monitors-only incident reads exactly as before
|
|
276
|
+
test("a monitors-only incident reads exactly as before", async () => {
|
|
250
277
|
await createIncidentFeed(
|
|
251
278
|
buildIncident({ monitors: [buildMonitor(MONITOR_ID, "checkout-web")] }),
|
|
252
279
|
);
|
|
@@ -254,7 +281,6 @@ describe("incident created feed item", () => {
|
|
|
254
281
|
expect(resourcesAffectedLines(postedMarkdown(incidentFeedItem))).toEqual([
|
|
255
282
|
`- [checkout-web](${monitorLink(MONITOR_ID)})`,
|
|
256
283
|
]);
|
|
257
|
-
expect(dashboardUrlLookup).not.toHaveBeenCalled();
|
|
258
284
|
});
|
|
259
285
|
|
|
260
286
|
test("an incident with neither monitors nor SLOs has no Resources Affected section", async () => {
|
|
@@ -328,7 +354,31 @@ describe("incident created feed item", () => {
|
|
|
328
354
|
expect(resourcesAffectedLines(postedMarkdown(incidentFeedItem))).toBeNull();
|
|
329
355
|
});
|
|
330
356
|
|
|
331
|
-
test("
|
|
357
|
+
test("reads the incident's SLOs with their project, as root", async () => {
|
|
358
|
+
await createIncidentFeed(buildIncident({}));
|
|
359
|
+
|
|
360
|
+
const sloRead: JSONObject | undefined = incidentRelationReads!.mock.calls
|
|
361
|
+
.map((call: Array<unknown>): JSONObject => {
|
|
362
|
+
return call[0] as JSONObject;
|
|
363
|
+
})
|
|
364
|
+
.find((findAllBy: JSONObject): boolean => {
|
|
365
|
+
return Boolean(
|
|
366
|
+
(findAllBy["select"] as JSONObject)["serviceLevelObjectives"],
|
|
367
|
+
);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
/*
|
|
371
|
+
* projectId too: the read runs as root, and the feed names only SLOs of
|
|
372
|
+
* the record's own project.
|
|
373
|
+
*/
|
|
374
|
+
expect(
|
|
375
|
+
(sloRead!["select"] as JSONObject)["serviceLevelObjectives"],
|
|
376
|
+
).toEqual({ _id: true, name: true, projectId: true });
|
|
377
|
+
expect(sloRead!["props"]).toEqual({ isRoot: true });
|
|
378
|
+
expect((sloRead!["query"] as JSONObject)["projectId"]).toBe(PROJECT_ID);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("onCreateSuccess leaves the relations to the feed builder's own reads", async () => {
|
|
332
382
|
jest.spyOn(ProductAnalytics, "captureForUser").mockImplementation((() => {
|
|
333
383
|
// no analytics in tests
|
|
334
384
|
}) as never);
|
|
@@ -362,45 +412,52 @@ describe("incident created feed item", () => {
|
|
|
362
412
|
).select;
|
|
363
413
|
|
|
364
414
|
/*
|
|
365
|
-
*
|
|
366
|
-
* the
|
|
415
|
+
* Joining every many-to-many relation into this one read would return
|
|
416
|
+
* the product of their sizes; LinkedAffectedResources reads them one at
|
|
417
|
+
* a time instead.
|
|
367
418
|
*/
|
|
368
|
-
expect(select["
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
projectId: true,
|
|
372
|
-
});
|
|
373
|
-
// The monitors the section already listed are still read.
|
|
374
|
-
expect(select["monitors"]).toEqual({ name: true, _id: true });
|
|
419
|
+
expect(select["monitors"]).toBeUndefined();
|
|
420
|
+
expect(select["serviceLevelObjectives"]).toBeUndefined();
|
|
421
|
+
expect(select["projectId"]).toBe(true);
|
|
375
422
|
});
|
|
376
423
|
});
|
|
377
424
|
|
|
378
425
|
describe("alert created feed item", () => {
|
|
426
|
+
let alertRelationReads: jest.SpyInstance;
|
|
427
|
+
|
|
379
428
|
function mockAlertRow(alert: Alert): jest.SpyInstance {
|
|
429
|
+
alertRelationReads = answerRelationReads(AlertService, alert);
|
|
430
|
+
|
|
380
431
|
return jest
|
|
381
432
|
.spyOn(AlertService, "findOneById")
|
|
382
433
|
.mockResolvedValue(alert as never);
|
|
383
434
|
}
|
|
384
435
|
|
|
385
|
-
test("reads the alert's SLOs alongside its monitor", async () => {
|
|
386
|
-
|
|
436
|
+
test("reads the alert's SLOs alongside its monitor, with their project", async () => {
|
|
437
|
+
mockAlertRow(buildAlert({}));
|
|
387
438
|
|
|
388
439
|
await createAlertFeed(ALERT_ID);
|
|
389
440
|
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
441
|
+
const selects: Array<JSONObject> = alertRelationReads.mock.calls.map(
|
|
442
|
+
(call: Array<unknown>): JSONObject => {
|
|
443
|
+
return (call[0] as { select: JSONObject }).select;
|
|
444
|
+
},
|
|
445
|
+
);
|
|
393
446
|
|
|
394
447
|
/*
|
|
395
|
-
* projectId too: the
|
|
396
|
-
* the record's own project.
|
|
448
|
+
* projectId too: the reads run as root, and the feed names only
|
|
449
|
+
* resources of the record's own project.
|
|
397
450
|
*/
|
|
398
|
-
expect(
|
|
399
|
-
|
|
451
|
+
expect(selects).toContainEqual({
|
|
452
|
+
_id: true,
|
|
453
|
+
projectId: true,
|
|
454
|
+
serviceLevelObjectives: { _id: true, name: true, projectId: true },
|
|
455
|
+
});
|
|
456
|
+
expect(selects).toContainEqual({
|
|
400
457
|
_id: true,
|
|
401
458
|
projectId: true,
|
|
459
|
+
monitor: { _id: true, name: true, projectId: true },
|
|
402
460
|
});
|
|
403
|
-
expect(select["monitor"]).toEqual({ name: true, _id: true });
|
|
404
461
|
});
|
|
405
462
|
|
|
406
463
|
test("a burn-rate alert with no monitor names its SLO under Resources Affected", async () => {
|
|
@@ -446,7 +503,6 @@ describe("alert created feed item", () => {
|
|
|
446
503
|
expect(resourcesAffectedLines(postedMarkdown(alertFeedItem))).toEqual([
|
|
447
504
|
`- [checkout-web](${monitorLink(MONITOR_ID)})`,
|
|
448
505
|
]);
|
|
449
|
-
expect(dashboardUrlLookup).not.toHaveBeenCalled();
|
|
450
506
|
});
|
|
451
507
|
|
|
452
508
|
test("an alert with neither monitor nor SLOs has no Resources Affected section", async () => {
|