@oneuptime/common 14.0.5 → 14.0.6

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.
Files changed (66) hide show
  1. package/Server/Services/MetricService.ts +1 -1
  2. package/Server/Types/Database/QueryHelper.ts +54 -1
  3. package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
  4. package/Server/Utils/AI/Toolbox/ResourceTools.ts +12 -1
  5. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
  6. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
  7. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +4 -3
  8. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +3 -1
  9. package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
  10. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
  11. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
  12. package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
  13. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
  14. package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
  15. package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
  16. package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
  17. package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
  18. package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
  19. package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
  20. package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
  21. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
  22. package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
  23. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +96 -0
  24. package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
  25. package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
  26. package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
  27. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +8 -14
  28. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +4 -3
  29. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +27 -6
  30. package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
  31. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
  32. package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
  33. package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
  34. package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
  35. package/Types/BaseDatabase/AggregationType.ts +7 -0
  36. package/Types/Dashboard/DashboardTemplates.ts +20 -7
  37. package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
  38. package/Types/Rum/WebVitals.ts +13 -0
  39. package/UI/Components/Input/Input.tsx +11 -0
  40. package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
  41. package/build/dist/Server/Services/MetricService.js +1 -1
  42. package/build/dist/Server/Types/Database/QueryHelper.js +47 -1
  43. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  44. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
  45. package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
  46. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +12 -1
  47. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  48. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
  49. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
  50. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  51. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
  52. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  53. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1 -1
  54. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  55. package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
  56. package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
  57. package/build/dist/Types/Dashboard/DashboardTemplates.js +20 -7
  58. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  59. package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
  60. package/build/dist/Types/Rum/WebVitals.js +12 -0
  61. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  62. package/build/dist/UI/Components/Input/Input.js +1 -1
  63. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  64. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
  65. package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
  66. package/package.json +1 -1
@@ -0,0 +1,169 @@
1
+ import StatusPageCertificateService from "../../../Server/Services/StatusPageCertificateService";
2
+ import DatabaseConfig from "../../../Server/DatabaseConfig";
3
+ import { AppApiHostname } from "../../../Server/EnvironmentConfig";
4
+ import API from "../../../Utils/API";
5
+ import Protocol from "../../../Types/API/Protocol";
6
+ import URL from "../../../Types/API/URL";
7
+ import { afterEach, describe, expect, test } from "@jest/globals";
8
+
9
+ /*
10
+ * Custom status page domains get their TLS certificate from the cert worker,
11
+ * and this service is the only caller. Nothing about it is visible until a
12
+ * customer's domain fails to serve HTTPS, so what these tests hold is the
13
+ * request itself: the verb, the worker route, the domain in the body, and the
14
+ * protocol taken from the instance's own configuration rather than assumed.
15
+ */
16
+
17
+ const WORKER_ROUTE: string = "/api/workers/cert";
18
+ const DOMAIN: string = "status.example.com";
19
+
20
+ type ApiCall = { url: URL; data: Record<string, unknown> };
21
+
22
+ function lastCall(spy: jest.SpyInstance): ApiCall {
23
+ return spy.mock.calls[spy.mock.calls.length - 1]?.[0] as ApiCall;
24
+ }
25
+
26
+ afterEach(() => {
27
+ jest.restoreAllMocks();
28
+ });
29
+
30
+ describe.each([
31
+ { label: "https", protocol: Protocol.HTTPS },
32
+ { label: "http", protocol: Protocol.HTTP },
33
+ ])(
34
+ "StatusPageCertificateService over $label",
35
+ ({ protocol }: { protocol: Protocol }) => {
36
+ function mockProtocol(): jest.SpyInstance {
37
+ return jest
38
+ .spyOn(DatabaseConfig, "getHttpProtocol")
39
+ .mockResolvedValue(protocol as never);
40
+ }
41
+
42
+ test("add posts the domain to the cert worker", async () => {
43
+ mockProtocol();
44
+ const postSpy: jest.SpyInstance = jest
45
+ .spyOn(API, "post")
46
+ .mockResolvedValue({} as never);
47
+
48
+ await StatusPageCertificateService.add(DOMAIN);
49
+
50
+ expect(postSpy).toHaveBeenCalledTimes(1);
51
+ const call: ApiCall = lastCall(postSpy);
52
+ expect(call.data).toEqual({ domain: DOMAIN });
53
+ expect(call.url.protocol).toBe(protocol);
54
+ expect(call.url.hostname.toString()).toBe(AppApiHostname.toString());
55
+ expect(call.url.route.toString()).toBe(WORKER_ROUTE);
56
+ });
57
+
58
+ test("remove deletes the domain at the same route", async () => {
59
+ mockProtocol();
60
+ const deleteSpy: jest.SpyInstance = jest
61
+ .spyOn(API, "delete")
62
+ .mockResolvedValue({} as never);
63
+
64
+ await StatusPageCertificateService.remove(DOMAIN);
65
+
66
+ expect(deleteSpy).toHaveBeenCalledTimes(1);
67
+ const call: ApiCall = lastCall(deleteSpy);
68
+ expect(call.data).toEqual({ domain: DOMAIN });
69
+ expect(call.url.protocol).toBe(protocol);
70
+ expect(call.url.route.toString()).toBe(WORKER_ROUTE);
71
+ });
72
+
73
+ test("get reads the domain's certificate from the same route", async () => {
74
+ mockProtocol();
75
+ const getSpy: jest.SpyInstance = jest
76
+ .spyOn(API, "get")
77
+ .mockResolvedValue({} as never);
78
+
79
+ await StatusPageCertificateService.get(DOMAIN);
80
+
81
+ expect(getSpy).toHaveBeenCalledTimes(1);
82
+ const call: ApiCall = lastCall(getSpy);
83
+ expect(call.data).toEqual({ domain: DOMAIN });
84
+ expect(call.url.protocol).toBe(protocol);
85
+ expect(call.url.route.toString()).toBe(WORKER_ROUTE);
86
+ });
87
+ },
88
+ );
89
+
90
+ describe("StatusPageCertificateService", () => {
91
+ test("each verb reads the protocol fresh rather than caching the first answer", async () => {
92
+ const protocolSpy: jest.SpyInstance = jest
93
+ .spyOn(DatabaseConfig, "getHttpProtocol")
94
+ .mockResolvedValueOnce(Protocol.HTTP as never)
95
+ .mockResolvedValueOnce(Protocol.HTTPS as never);
96
+ const postSpy: jest.SpyInstance = jest
97
+ .spyOn(API, "post")
98
+ .mockResolvedValue({} as never);
99
+
100
+ await StatusPageCertificateService.add(DOMAIN);
101
+ await StatusPageCertificateService.add(DOMAIN);
102
+
103
+ expect(protocolSpy).toHaveBeenCalledTimes(2);
104
+ expect((postSpy.mock.calls[0]?.[0] as ApiCall).url.protocol).toBe(
105
+ Protocol.HTTP,
106
+ );
107
+ expect((postSpy.mock.calls[1]?.[0] as ApiCall).url.protocol).toBe(
108
+ Protocol.HTTPS,
109
+ );
110
+ });
111
+
112
+ test("the three verbs do not call one another", async () => {
113
+ jest
114
+ .spyOn(DatabaseConfig, "getHttpProtocol")
115
+ .mockResolvedValue(Protocol.HTTPS as never);
116
+ const postSpy: jest.SpyInstance = jest
117
+ .spyOn(API, "post")
118
+ .mockResolvedValue({} as never);
119
+ const deleteSpy: jest.SpyInstance = jest
120
+ .spyOn(API, "delete")
121
+ .mockResolvedValue({} as never);
122
+ const getSpy: jest.SpyInstance = jest
123
+ .spyOn(API, "get")
124
+ .mockResolvedValue({} as never);
125
+
126
+ await StatusPageCertificateService.get(DOMAIN);
127
+
128
+ expect(getSpy).toHaveBeenCalledTimes(1);
129
+ expect(postSpy).not.toHaveBeenCalled();
130
+ expect(deleteSpy).not.toHaveBeenCalled();
131
+
132
+ await StatusPageCertificateService.remove(DOMAIN);
133
+
134
+ expect(deleteSpy).toHaveBeenCalledTimes(1);
135
+ expect(postSpy).not.toHaveBeenCalled();
136
+ });
137
+
138
+ test("the domain is passed through untouched, punycode and all", async () => {
139
+ jest
140
+ .spyOn(DatabaseConfig, "getHttpProtocol")
141
+ .mockResolvedValue(Protocol.HTTPS as never);
142
+ const postSpy: jest.SpyInstance = jest
143
+ .spyOn(API, "post")
144
+ .mockResolvedValue({} as never);
145
+
146
+ for (const domain of [
147
+ "status.example.com",
148
+ "xn--80ak6aa92e.example.com",
149
+ "STATUS.EXAMPLE.COM",
150
+ "status.example.co.uk",
151
+ ]) {
152
+ await StatusPageCertificateService.add(domain);
153
+ expect(lastCall(postSpy).data).toEqual({ domain: domain });
154
+ }
155
+ });
156
+
157
+ test("a failing worker call is surfaced, not swallowed", async () => {
158
+ jest
159
+ .spyOn(DatabaseConfig, "getHttpProtocol")
160
+ .mockResolvedValue(Protocol.HTTPS as never);
161
+ jest
162
+ .spyOn(API, "post")
163
+ .mockRejectedValue(new Error("cert worker unreachable") as never);
164
+
165
+ await expect(StatusPageCertificateService.add(DOMAIN)).rejects.toThrow(
166
+ "cert worker unreachable",
167
+ );
168
+ });
169
+ });
@@ -0,0 +1,191 @@
1
+ import TeamComplianceSettingService from "../../../Server/Services/TeamComplianceSettingService";
2
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
3
+ import { OnCreate } from "../../../Server/Types/Database/Hooks";
4
+ import TeamComplianceSetting from "../../../Models/DatabaseModels/TeamComplianceSetting";
5
+ import BadDataException from "../../../Types/Exception/BadDataException";
6
+ import ObjectID from "../../../Types/ObjectID";
7
+ import { afterEach, describe, expect, test } from "@jest/globals";
8
+
9
+ /*
10
+ * A team may hold at most one compliance setting per rule type: two rows for
11
+ * the same rule would leave the compliance check with no defined answer. The
12
+ * guard lives in onBeforeCreate, and these tests hold it to refusing loudly
13
+ * (BadDataException naming the rule type), to scoping the duplicate lookup by
14
+ * team *and* rule type -- so the same rule on another team is still allowed --
15
+ * and to running that lookup as root, since the row that already exists may be
16
+ * one the caller cannot see.
17
+ */
18
+
19
+ type OnBeforeCreateFunction = (
20
+ createBy: CreateBy<TeamComplianceSetting>,
21
+ ) => Promise<OnCreate<TeamComplianceSetting>>;
22
+
23
+ const TEAM_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
24
+ const OTHER_TEAM_ID: ObjectID = new ObjectID(
25
+ "22222222-2222-4222-8222-222222222222",
26
+ );
27
+ const RULE_TYPE: string = "RequireTwoFactorAuth";
28
+
29
+ function onBeforeCreate(
30
+ createBy: CreateBy<TeamComplianceSetting>,
31
+ ): Promise<OnCreate<TeamComplianceSetting>> {
32
+ return (
33
+ TeamComplianceSettingService as unknown as {
34
+ onBeforeCreate: OnBeforeCreateFunction;
35
+ }
36
+ ).onBeforeCreate(createBy);
37
+ }
38
+
39
+ function buildCreateBy(data: {
40
+ teamId?: ObjectID;
41
+ ruleType?: string;
42
+ }): CreateBy<TeamComplianceSetting> {
43
+ const setting: TeamComplianceSetting = new TeamComplianceSetting();
44
+ if (data.teamId) {
45
+ setting.teamId = data.teamId;
46
+ }
47
+ if (data.ruleType) {
48
+ (setting as unknown as Record<string, unknown>)["ruleType"] = data.ruleType;
49
+ }
50
+ return {
51
+ data: setting,
52
+ props: { isRoot: true },
53
+ } as CreateBy<TeamComplianceSetting>;
54
+ }
55
+
56
+ function mockExisting(
57
+ existing: TeamComplianceSetting | null,
58
+ ): jest.SpyInstance {
59
+ return jest
60
+ .spyOn(TeamComplianceSettingService, "findOneBy")
61
+ .mockResolvedValue(existing as never);
62
+ }
63
+
64
+ afterEach(() => {
65
+ jest.restoreAllMocks();
66
+ });
67
+
68
+ describe("TeamComplianceSettingService onBeforeCreate", () => {
69
+ test("allows the first setting for a team and rule type", async () => {
70
+ mockExisting(null);
71
+
72
+ const createBy: CreateBy<TeamComplianceSetting> = buildCreateBy({
73
+ teamId: TEAM_ID,
74
+ ruleType: RULE_TYPE,
75
+ });
76
+ const result: OnCreate<TeamComplianceSetting> =
77
+ await onBeforeCreate(createBy);
78
+
79
+ expect(result.createBy).toBe(createBy);
80
+ expect(result.carryForward).toBeNull();
81
+ });
82
+
83
+ test("refuses a second setting for the same team and rule type", async () => {
84
+ const existing: TeamComplianceSetting = new TeamComplianceSetting();
85
+ existing._id = "33333333-3333-4333-8333-333333333333";
86
+ mockExisting(existing);
87
+
88
+ await expect(
89
+ onBeforeCreate(buildCreateBy({ teamId: TEAM_ID, ruleType: RULE_TYPE })),
90
+ ).rejects.toThrow(BadDataException);
91
+ });
92
+
93
+ test("the refusal names the rule type, so the form can point at it", async () => {
94
+ const existing: TeamComplianceSetting = new TeamComplianceSetting();
95
+ existing._id = "33333333-3333-4333-8333-333333333333";
96
+ mockExisting(existing);
97
+
98
+ await expect(
99
+ onBeforeCreate(buildCreateBy({ teamId: TEAM_ID, ruleType: RULE_TYPE })),
100
+ ).rejects.toThrow(
101
+ `A compliance setting for rule type "${RULE_TYPE}" already exists for this team.`,
102
+ );
103
+ });
104
+
105
+ test("the duplicate lookup is keyed on both the team and the rule type", async () => {
106
+ const findSpy: jest.SpyInstance = mockExisting(null);
107
+
108
+ await onBeforeCreate(
109
+ buildCreateBy({ teamId: TEAM_ID, ruleType: RULE_TYPE }),
110
+ );
111
+
112
+ const findArgs: Record<string, unknown> = findSpy.mock
113
+ .calls[0]?.[0] as Record<string, unknown>;
114
+ expect(findArgs["query"]).toEqual({
115
+ teamId: TEAM_ID,
116
+ ruleType: RULE_TYPE,
117
+ });
118
+ // Only the id is needed to know whether one exists.
119
+ expect(findArgs["select"]).toEqual({ _id: true });
120
+ });
121
+
122
+ test("the lookup runs as root: the existing row may be invisible to the caller", async () => {
123
+ const findSpy: jest.SpyInstance = mockExisting(null);
124
+
125
+ await onBeforeCreate(
126
+ buildCreateBy({ teamId: TEAM_ID, ruleType: RULE_TYPE }),
127
+ );
128
+
129
+ const findArgs: Record<string, unknown> = findSpy.mock
130
+ .calls[0]?.[0] as Record<string, unknown>;
131
+ expect(findArgs["props"]).toEqual({ isRoot: true });
132
+ });
133
+
134
+ test("the same rule type on a different team is a different row", async () => {
135
+ const findSpy: jest.SpyInstance = jest
136
+ .spyOn(TeamComplianceSettingService, "findOneBy")
137
+ .mockImplementation(((
138
+ args: Record<string, unknown>,
139
+ ): Promise<unknown> => {
140
+ const query: Record<string, unknown> = args["query"] as Record<
141
+ string,
142
+ unknown
143
+ >;
144
+ // Only the first team already holds this rule.
145
+ if (query["teamId"] === TEAM_ID) {
146
+ const existing: TeamComplianceSetting = new TeamComplianceSetting();
147
+ existing._id = "33333333-3333-4333-8333-333333333333";
148
+ return Promise.resolve(existing);
149
+ }
150
+ return Promise.resolve(null);
151
+ }) as never);
152
+
153
+ await expect(
154
+ onBeforeCreate(buildCreateBy({ teamId: TEAM_ID, ruleType: RULE_TYPE })),
155
+ ).rejects.toThrow(BadDataException);
156
+
157
+ await expect(
158
+ onBeforeCreate(
159
+ buildCreateBy({ teamId: OTHER_TEAM_ID, ruleType: RULE_TYPE }),
160
+ ),
161
+ ).resolves.toBeDefined();
162
+
163
+ expect(findSpy).toHaveBeenCalledTimes(2);
164
+ });
165
+
166
+ test("a different rule type on the same team is allowed", async () => {
167
+ const findSpy: jest.SpyInstance = jest
168
+ .spyOn(TeamComplianceSettingService, "findOneBy")
169
+ .mockImplementation(((
170
+ args: Record<string, unknown>,
171
+ ): Promise<unknown> => {
172
+ const query: Record<string, unknown> = args["query"] as Record<
173
+ string,
174
+ unknown
175
+ >;
176
+ if (query["ruleType"] === RULE_TYPE) {
177
+ const existing: TeamComplianceSetting = new TeamComplianceSetting();
178
+ existing._id = "33333333-3333-4333-8333-333333333333";
179
+ return Promise.resolve(existing);
180
+ }
181
+ return Promise.resolve(null);
182
+ }) as never);
183
+
184
+ await expect(
185
+ onBeforeCreate(
186
+ buildCreateBy({ teamId: TEAM_ID, ruleType: "RequirePhoneNumber" }),
187
+ ),
188
+ ).resolves.toBeDefined();
189
+ expect(findSpy).toHaveBeenCalledTimes(1);
190
+ });
191
+ });