@oneuptime/common 12.0.19 → 12.0.21
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/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/EnvironmentConfig.ts +15 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/ProjectService.ts +104 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
- package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
- package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
- package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
- package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
- package/Tests/Server/Utils/Attribution.test.ts +4 -5
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
- package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
- package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
- package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +19 -12
- package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
- package/UI/Components/Charts/Line/LineChart.tsx +23 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
- package/Utils/Telemetry/CrossSignalScope.ts +19 -10
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Index.js +2 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +14 -5
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ChangeEventService.js +9 -0
- package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +88 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js +6 -6
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +2 -2
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +2 -2
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/Marketing/Attribution.js +9 -15
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +12 -4
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +114 -51
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +17 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
import EnterpriseLicenseAPI from "../../../Server/API/EnterpriseLicenseAPI";
|
|
2
|
+
import EnterpriseLicenseService from "../../../Server/Services/EnterpriseLicenseService";
|
|
3
|
+
import EnterpriseLicenseInstanceService from "../../../Server/Services/EnterpriseLicenseInstanceService";
|
|
4
|
+
import JSONWebToken from "../../../Server/Utils/JsonWebToken";
|
|
5
|
+
import Response from "../../../Server/Utils/Response";
|
|
6
|
+
import EnterpriseLicense from "../../../Models/DatabaseModels/EnterpriseLicense";
|
|
7
|
+
import EnterpriseLicenseInstance from "../../../Models/DatabaseModels/EnterpriseLicenseInstance";
|
|
8
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
9
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
10
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
11
|
+
import PositiveNumber from "../../../Types/PositiveNumber";
|
|
12
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
13
|
+
import EnterpriseLicenseSyncUtil, {
|
|
14
|
+
EnterpriseLicenseSyncResult,
|
|
15
|
+
} from "../../../Utils/EnterpriseLicense/EnterpriseLicenseSync";
|
|
16
|
+
import {
|
|
17
|
+
NextFunction,
|
|
18
|
+
OneUptimeRequest,
|
|
19
|
+
OneUptimeResponse,
|
|
20
|
+
} from "../../../Server/Utils/Express";
|
|
21
|
+
import { mockRouter } from "./Helpers";
|
|
22
|
+
import { beforeEach, afterEach, describe, expect, it } from "@jest/globals";
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* The license-server half of the seat-limit sync.
|
|
26
|
+
*
|
|
27
|
+
* /report-user-count is the daily call a self-hosted installation makes, and
|
|
28
|
+
* its response is the ONLY thing that installation can learn from without a
|
|
29
|
+
* human re-typing the license key. It used to answer with the usage numbers
|
|
30
|
+
* and the seat limit but nothing else, and the job on the other end dropped
|
|
31
|
+
* the seat limit on the floor — so raising a customer's limit on oneuptime.com
|
|
32
|
+
* never reached the installation enforcing it.
|
|
33
|
+
*
|
|
34
|
+
* These tests pin the response as a complete statement of the license terms,
|
|
35
|
+
* and pin the one thing that must NOT be unconditional: an expired license
|
|
36
|
+
* still gets a straight answer (it has to keep reporting, the expiry emails
|
|
37
|
+
* are built from those reports) but it does not get a fresh token.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/*
|
|
41
|
+
* PasswordHash carries a pre-existing TS diagnostic that fails any suite whose
|
|
42
|
+
* require graph reaches it, and DatabaseService — the base of every concrete
|
|
43
|
+
* service below — imports it. Replaced with a factory rather than automocked,
|
|
44
|
+
* because an automock still type-checks the real file.
|
|
45
|
+
*/
|
|
46
|
+
jest.mock("../../../Server/Utils/PasswordHash", () => {
|
|
47
|
+
return {
|
|
48
|
+
__esModule: true,
|
|
49
|
+
default: {
|
|
50
|
+
hash: jest.fn(),
|
|
51
|
+
verify: jest.fn(),
|
|
52
|
+
generateSalt: jest.fn(),
|
|
53
|
+
needsUpgrade: jest.fn(),
|
|
54
|
+
applyPepper: jest.fn(),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* Same story as PasswordHash: a pre-existing local-only TS diagnostic in a
|
|
61
|
+
* module the service graph drags in. Nothing here verifies a code.
|
|
62
|
+
*/
|
|
63
|
+
jest.mock("../../../Server/Utils/VerificationCode", () => {
|
|
64
|
+
return {
|
|
65
|
+
__esModule: true,
|
|
66
|
+
default: {
|
|
67
|
+
generate: jest.fn(),
|
|
68
|
+
hashCode: jest.fn(),
|
|
69
|
+
isHashEqual: jest.fn(),
|
|
70
|
+
generateUnusableHash: jest.fn(),
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
jest.mock("../../../Server/Utils/Express", () => {
|
|
76
|
+
return {
|
|
77
|
+
getRouter: () => {
|
|
78
|
+
return mockRouter;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
jest.mock("../../../Server/Utils/Response", () => {
|
|
84
|
+
return {
|
|
85
|
+
sendEntityArrayResponse: jest.fn().mockImplementation((...args: []) => {
|
|
86
|
+
return args;
|
|
87
|
+
}),
|
|
88
|
+
sendJsonObjectResponse: jest.fn().mockImplementation((...args: []) => {
|
|
89
|
+
return args;
|
|
90
|
+
}),
|
|
91
|
+
sendEmptySuccessResponse: jest.fn(),
|
|
92
|
+
sendEntityResponse: jest.fn().mockImplementation((...args: []) => {
|
|
93
|
+
return args;
|
|
94
|
+
}),
|
|
95
|
+
sendErrorResponse: jest.fn().mockImplementation((...args: []) => {
|
|
96
|
+
return args;
|
|
97
|
+
}),
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Signed with the server's own secret in production; here it only has to be
|
|
103
|
+
* deterministic and countable, so the expiry gating can be asserted on whether
|
|
104
|
+
* it was called at all.
|
|
105
|
+
*/
|
|
106
|
+
jest.mock("../../../Server/Utils/JsonWebToken", () => {
|
|
107
|
+
return {
|
|
108
|
+
__esModule: true,
|
|
109
|
+
default: {
|
|
110
|
+
signJsonPayload: jest.fn().mockReturnValue("signed.jwt.token"),
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
jest.mock("../../../Server/Services/EnterpriseLicenseService");
|
|
116
|
+
jest.mock("../../../Server/Services/EnterpriseLicenseInstanceService");
|
|
117
|
+
|
|
118
|
+
const REPORT_ROUTE: string = "/enterprise-license/report-user-count";
|
|
119
|
+
const VALIDATE_ROUTE: string = "/enterprise-license/validate";
|
|
120
|
+
|
|
121
|
+
const LICENSE_ID: ObjectID = ObjectID.generate();
|
|
122
|
+
const LICENSE_KEY: string = "acme-license-key";
|
|
123
|
+
|
|
124
|
+
// A hash the sanitizer accepts: 64 hex characters.
|
|
125
|
+
type HashFunction = (seed: string) => string;
|
|
126
|
+
|
|
127
|
+
const hashOf: HashFunction = (seed: string): string => {
|
|
128
|
+
return seed
|
|
129
|
+
.repeat(64)
|
|
130
|
+
.replace(/[^a-f0-9]/g, "a")
|
|
131
|
+
.substring(0, 64);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* Overrides are loosely typed on purpose: several tests need to express
|
|
136
|
+
* "this column is not set", which exactOptionalPropertyTypes forbids on
|
|
137
|
+
* Partial<EnterpriseLicense>.
|
|
138
|
+
*/
|
|
139
|
+
type MakeLicenseFunction = (
|
|
140
|
+
overrides?: Record<string, unknown>,
|
|
141
|
+
) => EnterpriseLicense;
|
|
142
|
+
|
|
143
|
+
const makeLicense: MakeLicenseFunction = (
|
|
144
|
+
overrides?: Record<string, unknown>,
|
|
145
|
+
): EnterpriseLicense => {
|
|
146
|
+
return {
|
|
147
|
+
id: LICENSE_ID,
|
|
148
|
+
companyName: "Acme Inc",
|
|
149
|
+
licenseKey: LICENSE_KEY,
|
|
150
|
+
expiresAt: OneUptimeDate.addRemoveDays(OneUptimeDate.getCurrentDate(), 90),
|
|
151
|
+
userLimit: 150,
|
|
152
|
+
currentUserCount: 42,
|
|
153
|
+
userCountUpdatedAt: OneUptimeDate.getCurrentDate(),
|
|
154
|
+
isEvaluationLicense: false,
|
|
155
|
+
...overrides,
|
|
156
|
+
} as unknown as EnterpriseLicense;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
type MakeInstanceFunction = (
|
|
160
|
+
overrides?: Record<string, unknown>,
|
|
161
|
+
) => EnterpriseLicenseInstance;
|
|
162
|
+
|
|
163
|
+
const makeInstance: MakeInstanceFunction = (
|
|
164
|
+
overrides?: Record<string, unknown>,
|
|
165
|
+
): EnterpriseLicenseInstance => {
|
|
166
|
+
return {
|
|
167
|
+
id: ObjectID.generate(),
|
|
168
|
+
instanceId: "instance-1",
|
|
169
|
+
host: "oneuptime.acme.internal",
|
|
170
|
+
userCount: 2,
|
|
171
|
+
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
172
|
+
lastReportedAt: OneUptimeDate.getCurrentDate(),
|
|
173
|
+
oneuptimeVersion: "12.0.19",
|
|
174
|
+
...overrides,
|
|
175
|
+
} as unknown as EnterpriseLicenseInstance;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
type GetResponseBodyFunction = () => JSONObject;
|
|
179
|
+
|
|
180
|
+
/*
|
|
181
|
+
* The third argument of the single sendJsonObjectResponse call: the literal
|
|
182
|
+
* body that goes on the wire.
|
|
183
|
+
*/
|
|
184
|
+
const getResponseBody: GetResponseBodyFunction = (): JSONObject => {
|
|
185
|
+
const calls: Array<Array<unknown>> = (
|
|
186
|
+
Response.sendJsonObjectResponse as unknown as jest.Mock
|
|
187
|
+
).mock.calls as Array<Array<unknown>>;
|
|
188
|
+
|
|
189
|
+
expect(calls).toHaveLength(1);
|
|
190
|
+
|
|
191
|
+
return calls[0]![2] as JSONObject;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
describe("EnterpriseLicenseAPI POST /enterprise-license/report-user-count", () => {
|
|
195
|
+
let mockRequest: OneUptimeRequest;
|
|
196
|
+
let mockResponse: OneUptimeResponse;
|
|
197
|
+
let nextFunction: NextFunction;
|
|
198
|
+
|
|
199
|
+
type CallRouteFunction = (route?: string) => Promise<void>;
|
|
200
|
+
|
|
201
|
+
const callRoute: CallRouteFunction = async (
|
|
202
|
+
route: string = REPORT_ROUTE,
|
|
203
|
+
): Promise<void> => {
|
|
204
|
+
await mockRouter
|
|
205
|
+
.match("post", route)
|
|
206
|
+
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
jest.clearAllMocks();
|
|
211
|
+
|
|
212
|
+
new EnterpriseLicenseAPI();
|
|
213
|
+
|
|
214
|
+
EnterpriseLicenseService.findOneBy = jest
|
|
215
|
+
.fn()
|
|
216
|
+
.mockResolvedValue(makeLicense());
|
|
217
|
+
EnterpriseLicenseService.updateOneById = jest
|
|
218
|
+
.fn()
|
|
219
|
+
.mockResolvedValue(undefined);
|
|
220
|
+
|
|
221
|
+
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([]);
|
|
222
|
+
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
223
|
+
.fn()
|
|
224
|
+
.mockResolvedValue(null);
|
|
225
|
+
EnterpriseLicenseInstanceService.updateOneById = jest
|
|
226
|
+
.fn()
|
|
227
|
+
.mockResolvedValue(undefined);
|
|
228
|
+
EnterpriseLicenseInstanceService.create = jest
|
|
229
|
+
.fn()
|
|
230
|
+
.mockResolvedValue(undefined);
|
|
231
|
+
EnterpriseLicenseInstanceService.countBy = jest
|
|
232
|
+
.fn()
|
|
233
|
+
.mockResolvedValue(new PositiveNumber(0));
|
|
234
|
+
|
|
235
|
+
mockRequest = {
|
|
236
|
+
body: {
|
|
237
|
+
licenseKey: LICENSE_KEY,
|
|
238
|
+
userCount: 2,
|
|
239
|
+
instanceId: "instance-1",
|
|
240
|
+
host: "oneuptime.acme.internal",
|
|
241
|
+
version: "12.0.19",
|
|
242
|
+
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
243
|
+
masterAdminEmails: ["admin@acme.com"],
|
|
244
|
+
},
|
|
245
|
+
} as unknown as OneUptimeRequest;
|
|
246
|
+
|
|
247
|
+
mockResponse = {
|
|
248
|
+
send: jest.fn(),
|
|
249
|
+
json: jest.fn(),
|
|
250
|
+
status: jest.fn().mockReturnThis(),
|
|
251
|
+
} as unknown as OneUptimeResponse;
|
|
252
|
+
|
|
253
|
+
nextFunction = jest.fn();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
afterEach(() => {
|
|
257
|
+
jest.restoreAllMocks();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe("the license terms it reports back", () => {
|
|
261
|
+
it("returns the seat limit, which is the field the installation was missing", async () => {
|
|
262
|
+
await callRoute();
|
|
263
|
+
|
|
264
|
+
expect(getResponseBody()["userLimit"]).toBe(150);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("returns null for a license with no seat limit, so the limit can be cleared", async () => {
|
|
268
|
+
EnterpriseLicenseService.findOneBy = jest
|
|
269
|
+
.fn()
|
|
270
|
+
.mockResolvedValue(makeLicense({ userLimit: undefined }));
|
|
271
|
+
|
|
272
|
+
await callRoute();
|
|
273
|
+
|
|
274
|
+
expect(getResponseBody()).toHaveProperty("userLimit", null);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("returns the company name and the expiry, so a renewal reaches the installation", async () => {
|
|
278
|
+
const expiresAt: Date = new Date("2027-01-01T00:00:00.000Z");
|
|
279
|
+
|
|
280
|
+
EnterpriseLicenseService.findOneBy = jest
|
|
281
|
+
.fn()
|
|
282
|
+
.mockResolvedValue(makeLicense({ expiresAt: expiresAt }));
|
|
283
|
+
|
|
284
|
+
await callRoute();
|
|
285
|
+
|
|
286
|
+
const body: JSONObject = getResponseBody();
|
|
287
|
+
|
|
288
|
+
expect(body["companyName"]).toBe("Acme Inc");
|
|
289
|
+
expect(body["expiresAt"]).toBe("2027-01-01T00:00:00.000Z");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("selects the term columns it reports - a trimmed select is how this broke", async () => {
|
|
293
|
+
await callRoute();
|
|
294
|
+
|
|
295
|
+
expect(EnterpriseLicenseService.findOneBy).toHaveBeenCalledWith(
|
|
296
|
+
expect.objectContaining({
|
|
297
|
+
query: { licenseKey: LICENSE_KEY },
|
|
298
|
+
select: expect.objectContaining({
|
|
299
|
+
companyName: true,
|
|
300
|
+
expiresAt: true,
|
|
301
|
+
licenseKey: true,
|
|
302
|
+
userLimit: true,
|
|
303
|
+
isEvaluationLicense: true,
|
|
304
|
+
}),
|
|
305
|
+
}),
|
|
306
|
+
);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("mirrors the evaluation flag", async () => {
|
|
310
|
+
EnterpriseLicenseService.findOneBy = jest
|
|
311
|
+
.fn()
|
|
312
|
+
.mockResolvedValue(makeLicense({ isEvaluationLicense: true }));
|
|
313
|
+
|
|
314
|
+
await callRoute();
|
|
315
|
+
|
|
316
|
+
expect(getResponseBody()["isEvaluationLicense"]).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("reports the usage numbers alongside the terms", async () => {
|
|
320
|
+
await callRoute();
|
|
321
|
+
|
|
322
|
+
const body: JSONObject = getResponseBody();
|
|
323
|
+
|
|
324
|
+
expect(body["currentUserCount"]).toBe(2);
|
|
325
|
+
expect(typeof body["userCountUpdatedAt"]).toBe("string");
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("the token", () => {
|
|
330
|
+
it("mints one for a live license", async () => {
|
|
331
|
+
await callRoute();
|
|
332
|
+
|
|
333
|
+
expect(getResponseBody()["token"]).toBe("signed.jwt.token");
|
|
334
|
+
expect(JSONWebToken.signJsonPayload).toHaveBeenCalledTimes(1);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("signs the current terms into it, not a stale copy", async () => {
|
|
338
|
+
await callRoute();
|
|
339
|
+
|
|
340
|
+
expect(JSONWebToken.signJsonPayload).toHaveBeenCalledWith(
|
|
341
|
+
expect.objectContaining({
|
|
342
|
+
companyName: "Acme Inc",
|
|
343
|
+
licenseKey: LICENSE_KEY,
|
|
344
|
+
userLimit: 150,
|
|
345
|
+
}),
|
|
346
|
+
expect.any(Number),
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("withholds it once the license has expired, rather than renewing it daily", async () => {
|
|
351
|
+
EnterpriseLicenseService.findOneBy = jest.fn().mockResolvedValue(
|
|
352
|
+
makeLicense({
|
|
353
|
+
expiresAt: OneUptimeDate.addRemoveDays(
|
|
354
|
+
OneUptimeDate.getCurrentDate(),
|
|
355
|
+
-1,
|
|
356
|
+
),
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
await callRoute();
|
|
361
|
+
|
|
362
|
+
expect(getResponseBody()).toHaveProperty("token", null);
|
|
363
|
+
expect(JSONWebToken.signJsonPayload).not.toHaveBeenCalled();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("still answers an expired license in full, because it must keep reporting", async () => {
|
|
367
|
+
const expiredAt: Date = OneUptimeDate.addRemoveDays(
|
|
368
|
+
OneUptimeDate.getCurrentDate(),
|
|
369
|
+
-1,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
EnterpriseLicenseService.findOneBy = jest
|
|
373
|
+
.fn()
|
|
374
|
+
.mockResolvedValue(makeLicense({ expiresAt: expiredAt }));
|
|
375
|
+
|
|
376
|
+
await callRoute();
|
|
377
|
+
|
|
378
|
+
const body: JSONObject = getResponseBody();
|
|
379
|
+
|
|
380
|
+
/*
|
|
381
|
+
* The expiry notification emails are built from what expired instances
|
|
382
|
+
* keep reporting, so this route deliberately does not reject them the
|
|
383
|
+
* way /validate does. Telling them the truth about the expiry is what
|
|
384
|
+
* makes the installation show itself as expired.
|
|
385
|
+
*/
|
|
386
|
+
expect(nextFunction).not.toHaveBeenCalled();
|
|
387
|
+
expect(body["expiresAt"]).toBe(expiredAt.toISOString());
|
|
388
|
+
expect(body["userLimit"]).toBe(150);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("withholds it for a license with no expiry set at all", async () => {
|
|
392
|
+
EnterpriseLicenseService.findOneBy = jest
|
|
393
|
+
.fn()
|
|
394
|
+
.mockResolvedValue(makeLicense({ expiresAt: undefined }));
|
|
395
|
+
|
|
396
|
+
await callRoute();
|
|
397
|
+
|
|
398
|
+
const body: JSONObject = getResponseBody();
|
|
399
|
+
|
|
400
|
+
expect(body).toHaveProperty("token", null);
|
|
401
|
+
expect(body).toHaveProperty("expiresAt", null);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("input validation", () => {
|
|
406
|
+
it("rejects a missing license key", async () => {
|
|
407
|
+
mockRequest.body["licenseKey"] = undefined;
|
|
408
|
+
|
|
409
|
+
await callRoute();
|
|
410
|
+
|
|
411
|
+
expect(nextFunction).toHaveBeenCalledWith(
|
|
412
|
+
new BadDataException("License key is required"),
|
|
413
|
+
);
|
|
414
|
+
expect(Response.sendJsonObjectResponse).not.toHaveBeenCalled();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it.each([
|
|
418
|
+
["a missing count", undefined],
|
|
419
|
+
["a negative count", -1],
|
|
420
|
+
["a fractional count", 1.5],
|
|
421
|
+
["a non-numeric count", "many"],
|
|
422
|
+
])("rejects %s", async (_label: string, value: unknown) => {
|
|
423
|
+
mockRequest.body["userCount"] = value;
|
|
424
|
+
|
|
425
|
+
await callRoute();
|
|
426
|
+
|
|
427
|
+
expect(nextFunction).toHaveBeenCalledWith(
|
|
428
|
+
new BadDataException("userCount must be a non-negative integer"),
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("accepts a count of zero", async () => {
|
|
433
|
+
mockRequest.body["userCount"] = 0;
|
|
434
|
+
mockRequest.body["userEmailHashes"] = [];
|
|
435
|
+
|
|
436
|
+
await callRoute();
|
|
437
|
+
|
|
438
|
+
expect(nextFunction).not.toHaveBeenCalled();
|
|
439
|
+
expect(getResponseBody()["currentUserCount"]).toBe(0);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("rejects an unknown license key", async () => {
|
|
443
|
+
EnterpriseLicenseService.findOneBy = jest.fn().mockResolvedValue(null);
|
|
444
|
+
|
|
445
|
+
await callRoute();
|
|
446
|
+
|
|
447
|
+
expect(nextFunction).toHaveBeenCalledWith(
|
|
448
|
+
new BadDataException("License key is invalid"),
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("stays unauthenticated - instances report before anyone signs in", () => {
|
|
453
|
+
expect(mockRouter.match("post", REPORT_ROUTE).middlewares).toHaveLength(
|
|
454
|
+
0,
|
|
455
|
+
);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
describe("usage accounting", () => {
|
|
460
|
+
it("records this instance's usage against the license", async () => {
|
|
461
|
+
await callRoute();
|
|
462
|
+
|
|
463
|
+
expect(EnterpriseLicenseInstanceService.create).toHaveBeenCalledWith(
|
|
464
|
+
expect.objectContaining({
|
|
465
|
+
data: expect.objectContaining({
|
|
466
|
+
instanceId: "instance-1",
|
|
467
|
+
userCount: 2,
|
|
468
|
+
}),
|
|
469
|
+
}),
|
|
470
|
+
);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("counts a user on two instances once", async () => {
|
|
474
|
+
const shared: string = hashOf("1");
|
|
475
|
+
|
|
476
|
+
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
477
|
+
makeInstance({
|
|
478
|
+
instanceId: "instance-1",
|
|
479
|
+
userCount: 2,
|
|
480
|
+
userEmailHashes: [shared, hashOf("2")],
|
|
481
|
+
}),
|
|
482
|
+
makeInstance({
|
|
483
|
+
instanceId: "instance-2",
|
|
484
|
+
userCount: 2,
|
|
485
|
+
userEmailHashes: [shared, hashOf("3")],
|
|
486
|
+
}),
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
await callRoute();
|
|
490
|
+
|
|
491
|
+
// Union of {1,2} and {1,3} is three seats, not four.
|
|
492
|
+
expect(getResponseBody()["currentUserCount"]).toBe(3);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("writes the deduplicated count back onto the license row", async () => {
|
|
496
|
+
EnterpriseLicenseInstanceService.findBy = jest
|
|
497
|
+
.fn()
|
|
498
|
+
.mockResolvedValue([makeInstance()]);
|
|
499
|
+
|
|
500
|
+
await callRoute();
|
|
501
|
+
|
|
502
|
+
expect(EnterpriseLicenseService.updateOneById).toHaveBeenCalledWith(
|
|
503
|
+
expect.objectContaining({
|
|
504
|
+
id: LICENSE_ID,
|
|
505
|
+
data: expect.objectContaining({ currentUserCount: 2 }),
|
|
506
|
+
}),
|
|
507
|
+
);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("never writes the seat limit back from a report", async () => {
|
|
511
|
+
/*
|
|
512
|
+
* The limit flows one way only: oneuptime.com to the installation. A
|
|
513
|
+
* report that could raise it would make the license meaningless.
|
|
514
|
+
*/
|
|
515
|
+
await callRoute();
|
|
516
|
+
|
|
517
|
+
const updateCall: JSONObject = (
|
|
518
|
+
EnterpriseLicenseService.updateOneById as unknown as jest.Mock
|
|
519
|
+
).mock.calls[0]![0] as JSONObject;
|
|
520
|
+
|
|
521
|
+
expect(Object.keys(updateCall["data"] as JSONObject)).toEqual([
|
|
522
|
+
"currentUserCount",
|
|
523
|
+
"userCountUpdatedAt",
|
|
524
|
+
]);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("leaves a decommissioned instance out of the seat count but still lists it", async () => {
|
|
528
|
+
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
529
|
+
makeInstance({
|
|
530
|
+
instanceId: "live",
|
|
531
|
+
userCount: 1,
|
|
532
|
+
userEmailHashes: [hashOf("1")],
|
|
533
|
+
}),
|
|
534
|
+
makeInstance({
|
|
535
|
+
instanceId: "retired",
|
|
536
|
+
userCount: 2,
|
|
537
|
+
userEmailHashes: [hashOf("2"), hashOf("3")],
|
|
538
|
+
lastReportedAt: OneUptimeDate.addRemoveDays(
|
|
539
|
+
OneUptimeDate.getCurrentDate(),
|
|
540
|
+
-400,
|
|
541
|
+
),
|
|
542
|
+
}),
|
|
543
|
+
]);
|
|
544
|
+
|
|
545
|
+
await callRoute();
|
|
546
|
+
|
|
547
|
+
const body: JSONObject = getResponseBody();
|
|
548
|
+
|
|
549
|
+
expect(body["currentUserCount"]).toBe(1);
|
|
550
|
+
expect(body["instances"]).toHaveLength(2);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("counts users an instance could not send hashes for, so a huge install is not undercounted", async () => {
|
|
554
|
+
/*
|
|
555
|
+
* Hash lists are capped, so an instance may report more users than
|
|
556
|
+
* hashes. The overflow cannot be deduplicated against anyone else and is
|
|
557
|
+
* counted as-is.
|
|
558
|
+
*/
|
|
559
|
+
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([
|
|
560
|
+
makeInstance({
|
|
561
|
+
instanceId: "big",
|
|
562
|
+
userCount: 10,
|
|
563
|
+
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
564
|
+
}),
|
|
565
|
+
]);
|
|
566
|
+
|
|
567
|
+
await callRoute();
|
|
568
|
+
|
|
569
|
+
expect(getResponseBody()["currentUserCount"]).toBe(10);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("does not let a legacy report with no instance id stomp the deduplicated count", async () => {
|
|
573
|
+
mockRequest.body["instanceId"] = undefined;
|
|
574
|
+
mockRequest.body["userCount"] = 99;
|
|
575
|
+
|
|
576
|
+
EnterpriseLicenseInstanceService.findBy = jest
|
|
577
|
+
.fn()
|
|
578
|
+
.mockResolvedValue([makeInstance()]);
|
|
579
|
+
|
|
580
|
+
await callRoute();
|
|
581
|
+
|
|
582
|
+
expect(getResponseBody()["currentUserCount"]).toBe(2);
|
|
583
|
+
expect(EnterpriseLicenseService.updateOneById).not.toHaveBeenCalled();
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
describe("EnterpriseLicenseAPI POST /enterprise-license/validate", () => {
|
|
589
|
+
let mockRequest: OneUptimeRequest;
|
|
590
|
+
let mockResponse: OneUptimeResponse;
|
|
591
|
+
let nextFunction: NextFunction;
|
|
592
|
+
|
|
593
|
+
beforeEach(() => {
|
|
594
|
+
jest.clearAllMocks();
|
|
595
|
+
|
|
596
|
+
new EnterpriseLicenseAPI();
|
|
597
|
+
|
|
598
|
+
EnterpriseLicenseService.findOneBy = jest
|
|
599
|
+
.fn()
|
|
600
|
+
.mockResolvedValue(makeLicense());
|
|
601
|
+
EnterpriseLicenseInstanceService.findBy = jest.fn().mockResolvedValue([]);
|
|
602
|
+
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
603
|
+
.fn()
|
|
604
|
+
.mockResolvedValue(null);
|
|
605
|
+
EnterpriseLicenseInstanceService.create = jest
|
|
606
|
+
.fn()
|
|
607
|
+
.mockResolvedValue(undefined);
|
|
608
|
+
EnterpriseLicenseInstanceService.countBy = jest
|
|
609
|
+
.fn()
|
|
610
|
+
.mockResolvedValue(new PositiveNumber(0));
|
|
611
|
+
|
|
612
|
+
mockRequest = {
|
|
613
|
+
body: {
|
|
614
|
+
licenseKey: LICENSE_KEY,
|
|
615
|
+
instanceId: "instance-1",
|
|
616
|
+
host: "oneuptime.acme.internal",
|
|
617
|
+
version: "12.0.19",
|
|
618
|
+
},
|
|
619
|
+
} as unknown as OneUptimeRequest;
|
|
620
|
+
|
|
621
|
+
mockResponse = {
|
|
622
|
+
send: jest.fn(),
|
|
623
|
+
json: jest.fn(),
|
|
624
|
+
status: jest.fn().mockReturnThis(),
|
|
625
|
+
} as unknown as OneUptimeResponse;
|
|
626
|
+
|
|
627
|
+
nextFunction = jest.fn();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
afterEach(() => {
|
|
631
|
+
jest.restoreAllMocks();
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
it("still answers with the same terms after the shared refactor", async () => {
|
|
635
|
+
const expiresAt: Date = OneUptimeDate.addRemoveDays(
|
|
636
|
+
OneUptimeDate.getCurrentDate(),
|
|
637
|
+
90,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
EnterpriseLicenseService.findOneBy = jest
|
|
641
|
+
.fn()
|
|
642
|
+
.mockResolvedValue(makeLicense({ expiresAt: expiresAt }));
|
|
643
|
+
|
|
644
|
+
await mockRouter
|
|
645
|
+
.match("post", VALIDATE_ROUTE)
|
|
646
|
+
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
647
|
+
|
|
648
|
+
expect(nextFunction).not.toHaveBeenCalled();
|
|
649
|
+
|
|
650
|
+
const body: JSONObject = getResponseBody();
|
|
651
|
+
|
|
652
|
+
expect(body["companyName"]).toBe("Acme Inc");
|
|
653
|
+
expect(body["licenseKey"]).toBe(LICENSE_KEY);
|
|
654
|
+
expect(body["userLimit"]).toBe(150);
|
|
655
|
+
expect(body["expiresAt"]).toBe(expiresAt.toISOString());
|
|
656
|
+
expect(body["token"]).toBe("signed.jwt.token");
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("keeps rejecting an expired license, unlike the report route", async () => {
|
|
660
|
+
EnterpriseLicenseService.findOneBy = jest.fn().mockResolvedValue(
|
|
661
|
+
makeLicense({
|
|
662
|
+
expiresAt: OneUptimeDate.addRemoveDays(
|
|
663
|
+
OneUptimeDate.getCurrentDate(),
|
|
664
|
+
-1,
|
|
665
|
+
),
|
|
666
|
+
}),
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
await mockRouter
|
|
670
|
+
.match("post", VALIDATE_ROUTE)
|
|
671
|
+
.handlerFunction(mockRequest, mockResponse, nextFunction);
|
|
672
|
+
|
|
673
|
+
expect(nextFunction).toHaveBeenCalledWith(
|
|
674
|
+
new BadDataException("License key has expired"),
|
|
675
|
+
);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
describe("the contract between the two halves of the sync", () => {
|
|
680
|
+
let nextFunction: NextFunction;
|
|
681
|
+
|
|
682
|
+
beforeEach(() => {
|
|
683
|
+
jest.clearAllMocks();
|
|
684
|
+
|
|
685
|
+
new EnterpriseLicenseAPI();
|
|
686
|
+
|
|
687
|
+
EnterpriseLicenseService.findOneBy = jest
|
|
688
|
+
.fn()
|
|
689
|
+
.mockResolvedValue(makeLicense());
|
|
690
|
+
EnterpriseLicenseService.updateOneById = jest
|
|
691
|
+
.fn()
|
|
692
|
+
.mockResolvedValue(undefined);
|
|
693
|
+
EnterpriseLicenseInstanceService.findBy = jest
|
|
694
|
+
.fn()
|
|
695
|
+
.mockResolvedValue([makeInstance()]);
|
|
696
|
+
EnterpriseLicenseInstanceService.findOneBy = jest
|
|
697
|
+
.fn()
|
|
698
|
+
.mockResolvedValue(null);
|
|
699
|
+
EnterpriseLicenseInstanceService.create = jest
|
|
700
|
+
.fn()
|
|
701
|
+
.mockResolvedValue(undefined);
|
|
702
|
+
EnterpriseLicenseInstanceService.countBy = jest
|
|
703
|
+
.fn()
|
|
704
|
+
.mockResolvedValue(new PositiveNumber(0));
|
|
705
|
+
|
|
706
|
+
nextFunction = jest.fn();
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
afterEach(() => {
|
|
710
|
+
jest.restoreAllMocks();
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it("lands every license term when the real response body is fed to the real mapper", async () => {
|
|
714
|
+
/*
|
|
715
|
+
* The end of the bug, asserted end to end: the literal object the license
|
|
716
|
+
* server puts on the wire, through the mapper the installation runs. If
|
|
717
|
+
* either side renames a field, this fails and nothing else has to.
|
|
718
|
+
*/
|
|
719
|
+
await mockRouter.match("post", REPORT_ROUTE).handlerFunction(
|
|
720
|
+
{
|
|
721
|
+
body: {
|
|
722
|
+
licenseKey: LICENSE_KEY,
|
|
723
|
+
userCount: 2,
|
|
724
|
+
instanceId: "instance-1",
|
|
725
|
+
host: "oneuptime.acme.internal",
|
|
726
|
+
version: "12.0.19",
|
|
727
|
+
userEmailHashes: [hashOf("1"), hashOf("2")],
|
|
728
|
+
masterAdminEmails: ["admin@acme.com"],
|
|
729
|
+
},
|
|
730
|
+
} as unknown as OneUptimeRequest,
|
|
731
|
+
{
|
|
732
|
+
send: jest.fn(),
|
|
733
|
+
json: jest.fn(),
|
|
734
|
+
status: jest.fn().mockReturnThis(),
|
|
735
|
+
} as unknown as OneUptimeResponse,
|
|
736
|
+
nextFunction,
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
expect(nextFunction).not.toHaveBeenCalled();
|
|
740
|
+
|
|
741
|
+
const wireBody: JSONObject = getResponseBody();
|
|
742
|
+
|
|
743
|
+
const result: EnterpriseLicenseSyncResult =
|
|
744
|
+
EnterpriseLicenseSyncUtil.getGlobalConfigUpdateFromLicenseResponse({
|
|
745
|
+
payload: wireBody,
|
|
746
|
+
reportedAt: OneUptimeDate.getCurrentDate(),
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
expect(result.warnings).toEqual([]);
|
|
750
|
+
expect(result.updateData.enterpriseLicenseUserLimit).toBe(150);
|
|
751
|
+
expect(result.updateData.enterpriseLicenseCurrentUserCount).toBe(2);
|
|
752
|
+
expect(result.updateData.enterpriseCompanyName).toBe("Acme Inc");
|
|
753
|
+
expect(result.updateData.enterpriseLicenseIsEvaluation).toBe(false);
|
|
754
|
+
expect(result.updateData.enterpriseLicenseToken).toBe("signed.jwt.token");
|
|
755
|
+
expect(result.updateData.enterpriseLicenseExpiresAt).toBeInstanceOf(Date);
|
|
756
|
+
expect(result.updateData.enterpriseLicenseInstances).toHaveLength(1);
|
|
757
|
+
|
|
758
|
+
// The installation never adopts a key handed to it by a response.
|
|
759
|
+
expect(Object.keys(result.updateData)).not.toContain(
|
|
760
|
+
"enterpriseLicenseKey",
|
|
761
|
+
);
|
|
762
|
+
});
|
|
763
|
+
});
|