@oneuptime/common 12.0.27 → 12.0.29
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/AnalyticsModels/RumSessionChunk.ts +52 -0
- package/Models/DatabaseModels/CloudResource.ts +17 -1
- package/Models/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/RumApplication.ts +35 -2
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/API/TelemetryAPI.ts +104 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
- package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Date.ts +143 -13
- package/Types/Rum/SessionReplay.ts +76 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +67 -14
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
2
|
+
import MonitorTemplate from "../../../Models/DatabaseModels/MonitorTemplate";
|
|
3
|
+
import MonitorService from "../../../Server/Services/MonitorService";
|
|
4
|
+
import MonitorTemplateService, {
|
|
5
|
+
SyncLinkedMonitorsResult,
|
|
6
|
+
} from "../../../Server/Services/MonitorTemplateService";
|
|
7
|
+
import UpdateBy from "../../../Server/Types/Database/UpdateBy";
|
|
8
|
+
import FindBy from "../../../Server/Types/Database/FindBy";
|
|
9
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
10
|
+
import Includes from "../../../Types/BaseDatabase/Includes";
|
|
11
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
12
|
+
import LIMIT_MAX from "../../../Types/Database/LimitMax";
|
|
13
|
+
import MonitorType from "../../../Types/Monitor/MonitorType";
|
|
14
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
15
|
+
import { afterEach, describe, expect, it, jest } from "@jest/globals";
|
|
16
|
+
import type { SpyInstance } from "jest-mock";
|
|
17
|
+
|
|
18
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
19
|
+
"11111111-1111-4111-8111-111111111111",
|
|
20
|
+
);
|
|
21
|
+
const TEMPLATE_ID: ObjectID = new ObjectID(
|
|
22
|
+
"22222222-2222-4222-8222-222222222222",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function buildPingTemplate(): MonitorTemplate {
|
|
26
|
+
const template: MonitorTemplate = new MonitorTemplate();
|
|
27
|
+
template.id = TEMPLATE_ID;
|
|
28
|
+
template.projectId = PROJECT_ID;
|
|
29
|
+
// Not a Network Device template, so sync takes the bulk update path.
|
|
30
|
+
template.monitorType = MonitorType.Ping;
|
|
31
|
+
template.monitoringInterval = "*/10 * * * *";
|
|
32
|
+
template.minimumProbeAgreement = 2;
|
|
33
|
+
return template;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Stand in for a project holding more linked monitors than a single update
|
|
38
|
+
* can carry. Pages are served by skip so the service's paging loop is what
|
|
39
|
+
* decides how many rows it discovers.
|
|
40
|
+
*/
|
|
41
|
+
function mockLinkedMonitorPages(totalLinkedMonitors: number): Array<ObjectID> {
|
|
42
|
+
/*
|
|
43
|
+
* A fixed pool rather than ids minted per page, so a test can assert which
|
|
44
|
+
* monitors were written and not merely how many — dropping one row while
|
|
45
|
+
* double-writing another keeps the counts intact.
|
|
46
|
+
*/
|
|
47
|
+
const pool: Array<ObjectID> = Array.from(
|
|
48
|
+
{ length: totalLinkedMonitors },
|
|
49
|
+
(): ObjectID => {
|
|
50
|
+
return ObjectID.generate();
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
jest
|
|
55
|
+
.spyOn(MonitorService, "findBy")
|
|
56
|
+
.mockImplementation(
|
|
57
|
+
async (findBy: FindBy<Monitor>): Promise<Array<Monitor>> => {
|
|
58
|
+
const skip: number = Number(findBy.skip?.toString() || 0);
|
|
59
|
+
const limit: number = Number(findBy.limit?.toString() || 0);
|
|
60
|
+
|
|
61
|
+
return pool.slice(skip, skip + limit).map((id: ObjectID): Monitor => {
|
|
62
|
+
const monitor: Monitor = new Monitor();
|
|
63
|
+
monitor.id = id;
|
|
64
|
+
monitor.projectId = PROJECT_ID;
|
|
65
|
+
monitor.monitorTemplateId = TEMPLATE_ID;
|
|
66
|
+
return monitor;
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
return pool;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writtenIdsFrom(
|
|
75
|
+
updateSpy: SpyInstance<typeof MonitorService.updateBy>,
|
|
76
|
+
): Array<string> {
|
|
77
|
+
return updateSpy.mock.calls.flatMap(
|
|
78
|
+
(call: [UpdateBy<Monitor>]): Array<string> => {
|
|
79
|
+
return (call[0].query as Record<string, Includes>)["_id"]!.values.map(
|
|
80
|
+
(id: string | ObjectID | number): string => {
|
|
81
|
+
return id.toString();
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
jest.restoreAllMocks();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("MonitorTemplateService bulk sync coverage", () => {
|
|
93
|
+
/*
|
|
94
|
+
* Regression: the bulk path issued one updateBy capped at LIMIT_MAX with no
|
|
95
|
+
* paging, so a template linked to more monitors than that silently left the
|
|
96
|
+
* remainder on the old configuration and still reported success.
|
|
97
|
+
*/
|
|
98
|
+
it("syncs every linked monitor when the fleet exceeds one update batch", async () => {
|
|
99
|
+
const totalLinkedMonitors: number = LIMIT_MAX * 2 + 500;
|
|
100
|
+
|
|
101
|
+
jest
|
|
102
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
103
|
+
.mockResolvedValue(buildPingTemplate());
|
|
104
|
+
jest
|
|
105
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
106
|
+
.mockResolvedValue(totalLinkedMonitors);
|
|
107
|
+
mockLinkedMonitorPages(totalLinkedMonitors);
|
|
108
|
+
|
|
109
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest
|
|
110
|
+
.spyOn(MonitorService, "updateBy")
|
|
111
|
+
.mockImplementation(
|
|
112
|
+
async (updateBy: UpdateBy<Monitor>): Promise<number> => {
|
|
113
|
+
const ids: Includes = (updateBy.query as Record<string, Includes>)[
|
|
114
|
+
"_id"
|
|
115
|
+
]!;
|
|
116
|
+
return ids.values.length;
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const result: SyncLinkedMonitorsResult =
|
|
121
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
122
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
123
|
+
fields: ["monitoringInterval"],
|
|
124
|
+
props: { isRoot: true },
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(result).toEqual({
|
|
128
|
+
totalLinkedMonitors: totalLinkedMonitors,
|
|
129
|
+
syncedMonitors: totalLinkedMonitors,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Three batches: LIMIT_MAX, LIMIT_MAX, then the 500 remainder.
|
|
133
|
+
expect(updateSpy).toHaveBeenCalledTimes(3);
|
|
134
|
+
expect(
|
|
135
|
+
updateSpy.mock.calls.map((call: [UpdateBy<Monitor>]): number => {
|
|
136
|
+
return (call[0].query as Record<string, Includes>)["_id"]!.values
|
|
137
|
+
.length;
|
|
138
|
+
}),
|
|
139
|
+
).toEqual([LIMIT_MAX, LIMIT_MAX, 500]);
|
|
140
|
+
|
|
141
|
+
// Every batch stays scoped to this template and project.
|
|
142
|
+
for (const call of updateSpy.mock.calls) {
|
|
143
|
+
const query: Record<string, unknown> = call[0].query as Record<
|
|
144
|
+
string,
|
|
145
|
+
unknown
|
|
146
|
+
>;
|
|
147
|
+
expect(query["monitorTemplateId"]).toEqual(TEMPLATE_ID);
|
|
148
|
+
expect(query["projectId"]).toEqual(PROJECT_ID);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("still issues a single batch for a fleet that fits in one update", async () => {
|
|
153
|
+
jest
|
|
154
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
155
|
+
.mockResolvedValue(buildPingTemplate());
|
|
156
|
+
jest
|
|
157
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
158
|
+
.mockResolvedValue(3);
|
|
159
|
+
mockLinkedMonitorPages(3);
|
|
160
|
+
|
|
161
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest
|
|
162
|
+
.spyOn(MonitorService, "updateBy")
|
|
163
|
+
.mockResolvedValue(3);
|
|
164
|
+
|
|
165
|
+
const result: SyncLinkedMonitorsResult =
|
|
166
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
167
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
168
|
+
fields: ["monitoringInterval"],
|
|
169
|
+
props: { isRoot: true },
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(result).toEqual({ totalLinkedMonitors: 3, syncedMonitors: 3 });
|
|
173
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("writes nothing when the template has no linked monitors", async () => {
|
|
177
|
+
jest
|
|
178
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
179
|
+
.mockResolvedValue(buildPingTemplate());
|
|
180
|
+
jest
|
|
181
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
182
|
+
.mockResolvedValue(0);
|
|
183
|
+
|
|
184
|
+
const findSpy: SpyInstance<typeof MonitorService.findBy> = jest.spyOn(
|
|
185
|
+
MonitorService,
|
|
186
|
+
"findBy",
|
|
187
|
+
);
|
|
188
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest.spyOn(
|
|
189
|
+
MonitorService,
|
|
190
|
+
"updateBy",
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const result: SyncLinkedMonitorsResult =
|
|
194
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
195
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
196
|
+
fields: ["monitoringInterval"],
|
|
197
|
+
props: { isRoot: true },
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(result).toEqual({ totalLinkedMonitors: 0, syncedMonitors: 0 });
|
|
201
|
+
expect(findSpy).not.toHaveBeenCalled();
|
|
202
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
/*
|
|
206
|
+
* Boundary: a full first page cannot be assumed to be the last one, so the
|
|
207
|
+
* loop must ask again and only stop on the short (here empty) page. Getting
|
|
208
|
+
* this wrong is how the original truncation behaved.
|
|
209
|
+
*/
|
|
210
|
+
it("covers a fleet of exactly one page without stopping early", async () => {
|
|
211
|
+
jest
|
|
212
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
213
|
+
.mockResolvedValue(buildPingTemplate());
|
|
214
|
+
jest
|
|
215
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
216
|
+
.mockResolvedValue(LIMIT_MAX);
|
|
217
|
+
mockLinkedMonitorPages(LIMIT_MAX);
|
|
218
|
+
|
|
219
|
+
const findSpy: SpyInstance<typeof MonitorService.findBy> = jest.spyOn(
|
|
220
|
+
MonitorService,
|
|
221
|
+
"findBy",
|
|
222
|
+
);
|
|
223
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest
|
|
224
|
+
.spyOn(MonitorService, "updateBy")
|
|
225
|
+
.mockImplementation(
|
|
226
|
+
async (updateBy: UpdateBy<Monitor>): Promise<number> => {
|
|
227
|
+
return (updateBy.query as Record<string, Includes>)["_id"]!.values
|
|
228
|
+
.length;
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const result: SyncLinkedMonitorsResult =
|
|
233
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
234
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
235
|
+
fields: ["monitoringInterval"],
|
|
236
|
+
props: { isRoot: true },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
expect(result).toEqual({
|
|
240
|
+
totalLinkedMonitors: LIMIT_MAX,
|
|
241
|
+
syncedMonitors: LIMIT_MAX,
|
|
242
|
+
});
|
|
243
|
+
// Full page, then the empty page that ends the loop.
|
|
244
|
+
expect(findSpy).toHaveBeenCalledTimes(2);
|
|
245
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("carries the single monitor that overflows a page into a second batch", async () => {
|
|
249
|
+
jest
|
|
250
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
251
|
+
.mockResolvedValue(buildPingTemplate());
|
|
252
|
+
jest
|
|
253
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
254
|
+
.mockResolvedValue(LIMIT_MAX + 1);
|
|
255
|
+
mockLinkedMonitorPages(LIMIT_MAX + 1);
|
|
256
|
+
|
|
257
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest
|
|
258
|
+
.spyOn(MonitorService, "updateBy")
|
|
259
|
+
.mockImplementation(
|
|
260
|
+
async (updateBy: UpdateBy<Monitor>): Promise<number> => {
|
|
261
|
+
return (updateBy.query as Record<string, Includes>)["_id"]!.values
|
|
262
|
+
.length;
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const result: SyncLinkedMonitorsResult =
|
|
267
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
268
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
269
|
+
fields: ["monitoringInterval"],
|
|
270
|
+
props: { isRoot: true },
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(result).toEqual({
|
|
274
|
+
totalLinkedMonitors: LIMIT_MAX + 1,
|
|
275
|
+
syncedMonitors: LIMIT_MAX + 1,
|
|
276
|
+
});
|
|
277
|
+
expect(
|
|
278
|
+
updateSpy.mock.calls.map((call: [UpdateBy<Monitor>]): number => {
|
|
279
|
+
return (call[0].query as Record<string, Includes>)["_id"]!.values
|
|
280
|
+
.length;
|
|
281
|
+
}),
|
|
282
|
+
).toEqual([LIMIT_MAX, 1]);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
/*
|
|
286
|
+
* countLinkedMonitors reads project-wide as root while the writes stay
|
|
287
|
+
* narrowed to the caller, so a label-scoped operator legitimately syncs
|
|
288
|
+
* fewer rows than are linked. The shortfall must reach the caller rather
|
|
289
|
+
* than being rounded up to the linked count.
|
|
290
|
+
*/
|
|
291
|
+
it("reports a permission-narrowed write honestly instead of claiming full coverage", async () => {
|
|
292
|
+
jest
|
|
293
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
294
|
+
.mockResolvedValue(buildPingTemplate());
|
|
295
|
+
jest
|
|
296
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
297
|
+
.mockResolvedValue(9);
|
|
298
|
+
mockLinkedMonitorPages(9);
|
|
299
|
+
|
|
300
|
+
jest.spyOn(MonitorService, "updateBy").mockResolvedValue(4);
|
|
301
|
+
|
|
302
|
+
const result: SyncLinkedMonitorsResult =
|
|
303
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
304
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
305
|
+
fields: ["monitoringInterval"],
|
|
306
|
+
props: { isRoot: false },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
expect(result).toEqual({ totalLinkedMonitors: 9, syncedMonitors: 4 });
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/*
|
|
313
|
+
* The per-monitor rebind path exists only to keep each Network Device
|
|
314
|
+
* monitor pointed at its own device, which is a monitorSteps concern. A
|
|
315
|
+
* sync that leaves steps alone has no reason to pay for it.
|
|
316
|
+
*/
|
|
317
|
+
it("takes the bulk path for a Network Device template when steps are not synced", async () => {
|
|
318
|
+
const template: MonitorTemplate = buildPingTemplate();
|
|
319
|
+
template.monitorType = MonitorType.NetworkDevice;
|
|
320
|
+
|
|
321
|
+
jest
|
|
322
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
323
|
+
.mockResolvedValue(template);
|
|
324
|
+
jest
|
|
325
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
326
|
+
.mockResolvedValue(2);
|
|
327
|
+
mockLinkedMonitorPages(2);
|
|
328
|
+
|
|
329
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest
|
|
330
|
+
.spyOn(MonitorService, "updateBy")
|
|
331
|
+
.mockResolvedValue(2);
|
|
332
|
+
const perMonitorSpy: SpyInstance<typeof MonitorService.updateOneById> =
|
|
333
|
+
jest.spyOn(MonitorService, "updateOneById");
|
|
334
|
+
|
|
335
|
+
const result: SyncLinkedMonitorsResult =
|
|
336
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
337
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
338
|
+
fields: ["monitoringInterval"],
|
|
339
|
+
props: { isRoot: true },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(result).toEqual({ totalLinkedMonitors: 2, syncedMonitors: 2 });
|
|
343
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
344
|
+
expect(perMonitorSpy).not.toHaveBeenCalled();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("writes nothing when the template carries none of the requested fields", async () => {
|
|
348
|
+
// Deliberately never assigns monitoringInterval, so buildUpdateData skips it.
|
|
349
|
+
const template: MonitorTemplate = new MonitorTemplate();
|
|
350
|
+
template.id = TEMPLATE_ID;
|
|
351
|
+
template.projectId = PROJECT_ID;
|
|
352
|
+
template.monitorType = MonitorType.Ping;
|
|
353
|
+
|
|
354
|
+
jest
|
|
355
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
356
|
+
.mockResolvedValue(template);
|
|
357
|
+
jest
|
|
358
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
359
|
+
.mockResolvedValue(5);
|
|
360
|
+
|
|
361
|
+
const findSpy: SpyInstance<typeof MonitorService.findBy> = jest.spyOn(
|
|
362
|
+
MonitorService,
|
|
363
|
+
"findBy",
|
|
364
|
+
);
|
|
365
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest.spyOn(
|
|
366
|
+
MonitorService,
|
|
367
|
+
"updateBy",
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const result: SyncLinkedMonitorsResult =
|
|
371
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
372
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
373
|
+
fields: ["monitoringInterval"],
|
|
374
|
+
props: { isRoot: true },
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(result).toEqual({ totalLinkedMonitors: 5, syncedMonitors: 0 });
|
|
378
|
+
expect(findSpy).not.toHaveBeenCalled();
|
|
379
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
/*
|
|
383
|
+
* The read enumerates as root so the reported linked total is project-wide,
|
|
384
|
+
* while every write carries the caller so updateBy can narrow it. Losing
|
|
385
|
+
* either half is a security-relevant change, and a count assertion cannot
|
|
386
|
+
* see it.
|
|
387
|
+
*/
|
|
388
|
+
it("enumerates ids as root while narrowing every write to the caller", async () => {
|
|
389
|
+
const callerProps: DatabaseCommonInteractionProps = {
|
|
390
|
+
tenantId: PROJECT_ID,
|
|
391
|
+
userId: ObjectID.generate(),
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
jest
|
|
395
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
396
|
+
.mockResolvedValue(buildPingTemplate());
|
|
397
|
+
jest
|
|
398
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
399
|
+
.mockResolvedValue(3);
|
|
400
|
+
mockLinkedMonitorPages(3);
|
|
401
|
+
|
|
402
|
+
const findSpy: SpyInstance<typeof MonitorService.findBy> = jest.spyOn(
|
|
403
|
+
MonitorService,
|
|
404
|
+
"findBy",
|
|
405
|
+
);
|
|
406
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest
|
|
407
|
+
.spyOn(MonitorService, "updateBy")
|
|
408
|
+
.mockResolvedValue(3);
|
|
409
|
+
|
|
410
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
411
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
412
|
+
fields: ["monitoringInterval"],
|
|
413
|
+
props: callerProps,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(findSpy.mock.calls[0]![0].props).toEqual({ isRoot: true });
|
|
417
|
+
for (const call of updateSpy.mock.calls) {
|
|
418
|
+
expect(call[0].props).toBe(callerProps);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
/*
|
|
423
|
+
* skip/limit paging is only stable over a total order, and a fleet
|
|
424
|
+
* provisioned by one auto-import run shares a createdAt. Without the _id
|
|
425
|
+
* tiebreaker a boundary tie returns one row twice and another never.
|
|
426
|
+
*/
|
|
427
|
+
it("pages linked ids under a total order scoped to the template", async () => {
|
|
428
|
+
const total: number = LIMIT_MAX * 2 + 5;
|
|
429
|
+
|
|
430
|
+
jest
|
|
431
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
432
|
+
.mockResolvedValue(buildPingTemplate());
|
|
433
|
+
jest
|
|
434
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
435
|
+
.mockResolvedValue(total);
|
|
436
|
+
mockLinkedMonitorPages(total);
|
|
437
|
+
|
|
438
|
+
const findSpy: SpyInstance<typeof MonitorService.findBy> = jest.spyOn(
|
|
439
|
+
MonitorService,
|
|
440
|
+
"findBy",
|
|
441
|
+
);
|
|
442
|
+
jest.spyOn(MonitorService, "updateBy").mockResolvedValue(0);
|
|
443
|
+
|
|
444
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
445
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
446
|
+
fields: ["monitoringInterval"],
|
|
447
|
+
props: { isRoot: true },
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(findSpy).toHaveBeenCalledWith(
|
|
451
|
+
expect.objectContaining({
|
|
452
|
+
query: { monitorTemplateId: TEMPLATE_ID, projectId: PROJECT_ID },
|
|
453
|
+
select: { _id: true },
|
|
454
|
+
sort: {
|
|
455
|
+
createdAt: SortOrder.Ascending,
|
|
456
|
+
_id: SortOrder.Ascending,
|
|
457
|
+
},
|
|
458
|
+
props: { isRoot: true },
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
// A dropped skip increment would hang rather than fail; pin the walk.
|
|
463
|
+
expect(
|
|
464
|
+
findSpy.mock.calls.map((call: [FindBy<Monitor>]): number => {
|
|
465
|
+
return Number(call[0].skip?.toString() || 0);
|
|
466
|
+
}),
|
|
467
|
+
).toEqual([0, LIMIT_MAX, LIMIT_MAX * 2]);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("writes every linked monitor exactly once across batches", async () => {
|
|
471
|
+
const total: number = LIMIT_MAX + 250;
|
|
472
|
+
|
|
473
|
+
jest
|
|
474
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
475
|
+
.mockResolvedValue(buildPingTemplate());
|
|
476
|
+
jest
|
|
477
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
478
|
+
.mockResolvedValue(total);
|
|
479
|
+
const pool: Array<ObjectID> = mockLinkedMonitorPages(total);
|
|
480
|
+
|
|
481
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest
|
|
482
|
+
.spyOn(MonitorService, "updateBy")
|
|
483
|
+
.mockResolvedValue(0);
|
|
484
|
+
|
|
485
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
486
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
487
|
+
fields: ["monitoringInterval"],
|
|
488
|
+
props: { isRoot: true },
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const written: Array<string> = writtenIdsFrom(updateSpy);
|
|
492
|
+
|
|
493
|
+
expect(written).toHaveLength(total);
|
|
494
|
+
expect(new Set(written).size).toBe(total);
|
|
495
|
+
expect(new Set(written)).toEqual(
|
|
496
|
+
new Set(
|
|
497
|
+
pool.map((id: ObjectID): string => {
|
|
498
|
+
return id.toString();
|
|
499
|
+
}),
|
|
500
|
+
),
|
|
501
|
+
);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
/*
|
|
505
|
+
* Batch width and the update limit are the same constant today. If they ever
|
|
506
|
+
* diverge, updateBy silently truncates each batch and the original bug comes
|
|
507
|
+
* back green.
|
|
508
|
+
*/
|
|
509
|
+
it("never hands updateBy more ids than its own limit allows", async () => {
|
|
510
|
+
const total: number = LIMIT_MAX * 2 + 7;
|
|
511
|
+
|
|
512
|
+
jest
|
|
513
|
+
.spyOn(MonitorTemplateService, "findOneById")
|
|
514
|
+
.mockResolvedValue(buildPingTemplate());
|
|
515
|
+
jest
|
|
516
|
+
.spyOn(MonitorTemplateService, "countLinkedMonitors")
|
|
517
|
+
.mockResolvedValue(total);
|
|
518
|
+
mockLinkedMonitorPages(total);
|
|
519
|
+
|
|
520
|
+
const updateSpy: SpyInstance<typeof MonitorService.updateBy> = jest
|
|
521
|
+
.spyOn(MonitorService, "updateBy")
|
|
522
|
+
.mockResolvedValue(0);
|
|
523
|
+
|
|
524
|
+
await MonitorTemplateService.syncLinkedMonitors({
|
|
525
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
526
|
+
fields: ["monitoringInterval"],
|
|
527
|
+
props: { isRoot: true },
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
for (const call of updateSpy.mock.calls) {
|
|
531
|
+
const batchSize: number = (call[0].query as Record<string, Includes>)[
|
|
532
|
+
"_id"
|
|
533
|
+
]!.values.length;
|
|
534
|
+
expect(Number(call[0].limit)).toBeGreaterThanOrEqual(batchSize);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("rejects a field that is not syncable from a template", async () => {
|
|
539
|
+
await expect(
|
|
540
|
+
MonitorTemplateService.syncLinkedMonitors({
|
|
541
|
+
monitorTemplateId: TEMPLATE_ID,
|
|
542
|
+
fields: ["monitorName"],
|
|
543
|
+
props: { isRoot: true },
|
|
544
|
+
}),
|
|
545
|
+
).rejects.toThrow(
|
|
546
|
+
'Field "monitorName" is not syncable from a monitor template',
|
|
547
|
+
);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
@@ -366,8 +366,21 @@ describe("MonitorTemplateService Network Device synchronization", () => {
|
|
|
366
366
|
const bulkUpdateSpy: SpyInstance<typeof MonitorService.updateBy> = jest
|
|
367
367
|
.spyOn(MonitorService, "updateBy")
|
|
368
368
|
.mockResolvedValue(3);
|
|
369
|
-
|
|
370
|
-
|
|
369
|
+
/*
|
|
370
|
+
* The bulk path reads ids so it can cover a fleet larger than one update
|
|
371
|
+
* batch, but it must still write through updateBy rather than rebinding
|
|
372
|
+
* each monitor the way the Network Device path does.
|
|
373
|
+
*/
|
|
374
|
+
jest
|
|
375
|
+
.spyOn(MonitorService, "findBy")
|
|
376
|
+
.mockResolvedValue([
|
|
377
|
+
buildLinkedMonitor([DEVICE_ONE_ID]),
|
|
378
|
+
buildLinkedMonitor([DEVICE_ONE_ID]),
|
|
379
|
+
buildLinkedMonitor([DEVICE_TWO_ID]),
|
|
380
|
+
]);
|
|
381
|
+
const perMonitorUpdateSpy: SpyInstance<
|
|
382
|
+
typeof MonitorService.updateOneById
|
|
383
|
+
> = jest.spyOn(MonitorService, "updateOneById");
|
|
371
384
|
|
|
372
385
|
const result: SyncLinkedMonitorsResult =
|
|
373
386
|
await MonitorTemplateService.syncLinkedMonitors({
|
|
@@ -378,6 +391,6 @@ describe("MonitorTemplateService Network Device synchronization", () => {
|
|
|
378
391
|
|
|
379
392
|
expect(result).toEqual({ totalLinkedMonitors: 3, syncedMonitors: 3 });
|
|
380
393
|
expect(bulkUpdateSpy).toHaveBeenCalledTimes(1);
|
|
381
|
-
expect(
|
|
394
|
+
expect(perMonitorUpdateSpy).not.toHaveBeenCalled();
|
|
382
395
|
});
|
|
383
396
|
});
|
|
@@ -785,6 +785,63 @@ describe("NetworkDeviceAutoImportRuleEngineService.processCompletedScan", () =>
|
|
|
785
785
|
).toBe(TEMPLATE_DEVICE_ID);
|
|
786
786
|
});
|
|
787
787
|
|
|
788
|
+
/*
|
|
789
|
+
* ISSUE #3486. "Default Monitor Name" is optional, and a template that
|
|
790
|
+
* leaves it blank names what it provisions after the DEVICE and nothing
|
|
791
|
+
* else - the whole complaint being that every auto-imported device came
|
|
792
|
+
* back carrying the same fixed suffix.
|
|
793
|
+
*
|
|
794
|
+
* Driven end to end through the engine rather than through
|
|
795
|
+
* NetworkDeviceMonitorTemplateUtil alone, because the name only survives
|
|
796
|
+
* to the create call if `monitorName` is still in this service's template
|
|
797
|
+
* select; a dropped select column looks exactly like a blank stored value
|
|
798
|
+
* and would silently un-suffix every project's monitors.
|
|
799
|
+
*/
|
|
800
|
+
it("names the provisioned monitor after the device alone when the template has no default name", async () => {
|
|
801
|
+
scanFindOneByMock.mockResolvedValue(
|
|
802
|
+
makeScan({ discoveredDevices: [makeHost()] }),
|
|
803
|
+
);
|
|
804
|
+
ruleFindByMock.mockResolvedValue([
|
|
805
|
+
makeRule({ monitorTemplateId: TEMPLATE_ID }),
|
|
806
|
+
]);
|
|
807
|
+
monitorTemplateFindByMock.mockResolvedValue([
|
|
808
|
+
makeTemplate({ monitorName: undefined }),
|
|
809
|
+
]);
|
|
810
|
+
|
|
811
|
+
const result: AutoImportRuleRunResult | null = await processScan();
|
|
812
|
+
|
|
813
|
+
expect(result).toMatchObject({ monitorsCreated: 1, monitorsFailed: 0 });
|
|
814
|
+
|
|
815
|
+
const monitor: Monitor = provisionedMonitor(0);
|
|
816
|
+
expect(monitor.name).toBe("core-switch-01");
|
|
817
|
+
expect(monitor.name).not.toContain(" - ");
|
|
818
|
+
|
|
819
|
+
// The read that makes the suffix possible at all.
|
|
820
|
+
expect(monitorTemplateFindByMock.mock.calls[0]![0].select).toMatchObject({
|
|
821
|
+
monitorName: true,
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it("names the provisioned monitor after the device alone when the stored default name is empty", async () => {
|
|
826
|
+
scanFindOneByMock.mockResolvedValue(
|
|
827
|
+
makeScan({ discoveredDevices: [makeHost()] }),
|
|
828
|
+
);
|
|
829
|
+
ruleFindByMock.mockResolvedValue([
|
|
830
|
+
makeRule({ monitorTemplateId: TEMPLATE_ID }),
|
|
831
|
+
]);
|
|
832
|
+
/*
|
|
833
|
+
* Reachable without any migration backfill: the dashboard's edit form
|
|
834
|
+
* PUTs an empty string, not null, when the operator clears the box.
|
|
835
|
+
*/
|
|
836
|
+
monitorTemplateFindByMock.mockResolvedValue([
|
|
837
|
+
makeTemplate({ monitorName: "" }),
|
|
838
|
+
]);
|
|
839
|
+
|
|
840
|
+
await processScan();
|
|
841
|
+
|
|
842
|
+
expect(provisionedMonitor(0).name).toBe("core-switch-01");
|
|
843
|
+
});
|
|
844
|
+
|
|
788
845
|
it("backfills a selected template monitor for an already-registered matching device", async () => {
|
|
789
846
|
const existingDevice: NetworkDevice = makeExistingDevice();
|
|
790
847
|
scanFindOneByMock.mockResolvedValue(
|