@oneuptime/common 11.3.5 → 11.3.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.
- package/Server/Infrastructure/Queue.ts +103 -16
- package/Server/Services/AnalyticsDatabaseService.ts +9 -5
- package/Server/Services/TeamMemberService.ts +38 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +31 -0
- package/Server/Utils/Monitor/IncomingRequestIncidentGrouping.ts +428 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +158 -8
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +26 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +147 -2
- package/Server/Utils/Monitor/MonitorResource.ts +74 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +2 -1
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +32 -0
- package/Tests/Server/Utils/Monitor/IncomingRequestIncidentGrouping.test.ts +389 -0
- package/Tests/Server/Utils/Monitor/SeriesAbsenceResolutionGuard.test.ts +177 -0
- package/Tests/Types/Events/Recurring.test.ts +475 -0
- package/Tests/Types/Monitor/CriteriaFilter.test.ts +348 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +331 -0
- package/Tests/UI/Components/Alert.test.tsx +23 -15
- package/Tests/UI/Components/Breadcrumbs.test.tsx +18 -6
- package/Tests/UI/Components/Button.test.tsx +3 -3
- package/Tests/UI/Components/DictionaryFilterOperator.test.ts +174 -0
- package/Tests/UI/Components/EmptyState/EmptyState.test.tsx +4 -3
- package/Tests/UI/Components/Input.test.tsx +3 -2
- package/Tests/UI/Components/Pill.test.tsx +4 -2
- package/Tests/UI/Components/SideMenuItem.test.tsx +18 -8
- package/Tests/UI/Components/TextArea.test.tsx +6 -4
- package/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.ts +74 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +22 -0
- package/UI/Components/Dictionary/Dictionary.tsx +123 -50
- package/UI/Components/Dictionary/DictionaryFilterOperator.ts +96 -1
- package/UI/Components/MasterPage/MasterPage.tsx +5 -3
- package/build/dist/Server/Infrastructure/Queue.js +83 -15
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +9 -5
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +37 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +26 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js +288 -0
- package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +119 -8
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +24 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +103 -2
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +62 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js +8 -0
- package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +10 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +60 -9
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +69 -0
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +5 -3
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CheckOn,
|
|
3
|
+
CriteriaFilter,
|
|
4
|
+
CriteriaFilterUtil,
|
|
5
|
+
EvaluateOverTimeType,
|
|
6
|
+
FilterType,
|
|
7
|
+
} from "../../../Types/Monitor/CriteriaFilter";
|
|
8
|
+
|
|
9
|
+
describe("CriteriaFilterUtil", () => {
|
|
10
|
+
describe("isAnomalyFilterType", () => {
|
|
11
|
+
test.each([
|
|
12
|
+
FilterType.AnomalouslyHigh,
|
|
13
|
+
FilterType.AnomalouslyLow,
|
|
14
|
+
FilterType.Anomalous,
|
|
15
|
+
])("returns true for %s", (filterType: FilterType) => {
|
|
16
|
+
expect(CriteriaFilterUtil.isAnomalyFilterType(filterType)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test.each([FilterType.EqualTo, FilterType.GreaterThan])(
|
|
20
|
+
"returns false for %s",
|
|
21
|
+
(filterType: FilterType) => {
|
|
22
|
+
expect(CriteriaFilterUtil.isAnomalyFilterType(filterType)).toBe(false);
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
test("returns false for undefined", () => {
|
|
27
|
+
expect(CriteriaFilterUtil.isAnomalyFilterType(undefined)).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe("hasValueField", () => {
|
|
32
|
+
test("returns true for a plain threshold filter", () => {
|
|
33
|
+
expect(
|
|
34
|
+
CriteriaFilterUtil.hasValueField({
|
|
35
|
+
checkOn: CheckOn.ResponseTime,
|
|
36
|
+
filterType: FilterType.GreaterThan,
|
|
37
|
+
}),
|
|
38
|
+
).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns false for anomaly filter types", () => {
|
|
42
|
+
expect(
|
|
43
|
+
CriteriaFilterUtil.hasValueField({
|
|
44
|
+
checkOn: CheckOn.MetricValue,
|
|
45
|
+
filterType: FilterType.AnomalouslyHigh,
|
|
46
|
+
}),
|
|
47
|
+
).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test.each([
|
|
51
|
+
CheckOn.IsOnline,
|
|
52
|
+
CheckOn.SnmpIsOnline,
|
|
53
|
+
CheckOn.DnsIsOnline,
|
|
54
|
+
CheckOn.DomainIsExpired,
|
|
55
|
+
CheckOn.IsRequestTimeout,
|
|
56
|
+
CheckOn.SnmpOidExists,
|
|
57
|
+
CheckOn.IsValidCertificate,
|
|
58
|
+
])("returns false for boolean-style checkOn %s", (checkOn: CheckOn) => {
|
|
59
|
+
expect(
|
|
60
|
+
CriteriaFilterUtil.hasValueField({
|
|
61
|
+
checkOn,
|
|
62
|
+
filterType: FilterType.True,
|
|
63
|
+
}),
|
|
64
|
+
).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test.each([
|
|
68
|
+
FilterType.IsEmpty,
|
|
69
|
+
FilterType.IsNotEmpty,
|
|
70
|
+
FilterType.True,
|
|
71
|
+
FilterType.False,
|
|
72
|
+
])(
|
|
73
|
+
"returns false for valueless filter type %s",
|
|
74
|
+
(filterType: FilterType) => {
|
|
75
|
+
expect(
|
|
76
|
+
CriteriaFilterUtil.hasValueField({
|
|
77
|
+
checkOn: CheckOn.ResponseBody,
|
|
78
|
+
filterType,
|
|
79
|
+
}),
|
|
80
|
+
).toBe(false);
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
test.each([
|
|
85
|
+
CheckOn.DnssecChainValid,
|
|
86
|
+
CheckOn.DnssecDnskeyExists,
|
|
87
|
+
CheckOn.DnssecDsExists,
|
|
88
|
+
CheckOn.DnssecResolverConsensus,
|
|
89
|
+
CheckOn.DnssecNameserverConsistent,
|
|
90
|
+
CheckOn.ExternalStatusPageIsOnline,
|
|
91
|
+
])(
|
|
92
|
+
"returns false for the rest of the boolean checkOn group %s",
|
|
93
|
+
(checkOn: CheckOn) => {
|
|
94
|
+
expect(
|
|
95
|
+
CriteriaFilterUtil.hasValueField({
|
|
96
|
+
checkOn,
|
|
97
|
+
filterType: FilterType.GreaterThan,
|
|
98
|
+
}),
|
|
99
|
+
).toBe(false);
|
|
100
|
+
},
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
test.each([CheckOn.DnssecIsValid, CheckOn.DnsRecordExists])(
|
|
104
|
+
"returns false for the existence-style checkOn %s",
|
|
105
|
+
(checkOn: CheckOn) => {
|
|
106
|
+
expect(
|
|
107
|
+
CriteriaFilterUtil.hasValueField({
|
|
108
|
+
checkOn,
|
|
109
|
+
filterType: FilterType.GreaterThan,
|
|
110
|
+
}),
|
|
111
|
+
).toBe(false);
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
test.each([
|
|
116
|
+
FilterType.AnomalouslyHigh,
|
|
117
|
+
FilterType.AnomalouslyLow,
|
|
118
|
+
FilterType.Anomalous,
|
|
119
|
+
])(
|
|
120
|
+
"anomaly filter type %s short-circuits even for a normally-valued checkOn",
|
|
121
|
+
(filterType: FilterType) => {
|
|
122
|
+
expect(
|
|
123
|
+
CriteriaFilterUtil.hasValueField({
|
|
124
|
+
checkOn: CheckOn.ResponseTime,
|
|
125
|
+
filterType,
|
|
126
|
+
}),
|
|
127
|
+
).toBe(false);
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
test("returns true for ResponseTime with GreaterThan", () => {
|
|
132
|
+
expect(
|
|
133
|
+
CriteriaFilterUtil.hasValueField({
|
|
134
|
+
checkOn: CheckOn.ResponseTime,
|
|
135
|
+
filterType: FilterType.GreaterThan,
|
|
136
|
+
}),
|
|
137
|
+
).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("returns true for ResponseStatusCode with EqualTo", () => {
|
|
141
|
+
expect(
|
|
142
|
+
CriteriaFilterUtil.hasValueField({
|
|
143
|
+
checkOn: CheckOn.ResponseStatusCode,
|
|
144
|
+
filterType: FilterType.EqualTo,
|
|
145
|
+
}),
|
|
146
|
+
).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("returns true for a string-comparison filter that falls through all guards", () => {
|
|
150
|
+
expect(
|
|
151
|
+
CriteriaFilterUtil.hasValueField({
|
|
152
|
+
checkOn: CheckOn.ResponseBody,
|
|
153
|
+
filterType: FilterType.Contains,
|
|
154
|
+
}),
|
|
155
|
+
).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("returns true when filterType is undefined and checkOn is not valueless", () => {
|
|
159
|
+
expect(
|
|
160
|
+
CriteriaFilterUtil.hasValueField({
|
|
161
|
+
checkOn: CheckOn.ResponseTime,
|
|
162
|
+
filterType: undefined,
|
|
163
|
+
}),
|
|
164
|
+
).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test.each([
|
|
168
|
+
CheckOn.IsExpiredCertificate,
|
|
169
|
+
CheckOn.IsNotAValidCertificate,
|
|
170
|
+
CheckOn.IsSelfSignedCertificate,
|
|
171
|
+
])(
|
|
172
|
+
"returns false for the rest of the certificate checkOn group %s",
|
|
173
|
+
(checkOn: CheckOn) => {
|
|
174
|
+
expect(
|
|
175
|
+
CriteriaFilterUtil.hasValueField({
|
|
176
|
+
checkOn,
|
|
177
|
+
filterType: FilterType.GreaterThan,
|
|
178
|
+
}),
|
|
179
|
+
).toBe(false);
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("getEvaluateOverTimeTypeByCriteriaFilter", () => {
|
|
185
|
+
test("returns an empty list when no filter is provided", () => {
|
|
186
|
+
expect(
|
|
187
|
+
CriteriaFilterUtil.getEvaluateOverTimeTypeByCriteriaFilter(undefined),
|
|
188
|
+
).toEqual([]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("returns AllValues / AnyValue for IsOnline", () => {
|
|
192
|
+
const result: Array<EvaluateOverTimeType> =
|
|
193
|
+
CriteriaFilterUtil.getEvaluateOverTimeTypeByCriteriaFilter({
|
|
194
|
+
checkOn: CheckOn.IsOnline,
|
|
195
|
+
filterType: FilterType.True,
|
|
196
|
+
value: undefined,
|
|
197
|
+
} as CriteriaFilter);
|
|
198
|
+
|
|
199
|
+
expect(result).toEqual([
|
|
200
|
+
EvaluateOverTimeType.AllValues,
|
|
201
|
+
EvaluateOverTimeType.AnyValue,
|
|
202
|
+
]);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("returns the full set of aggregation types for a numeric filter", () => {
|
|
206
|
+
const result: Array<EvaluateOverTimeType> =
|
|
207
|
+
CriteriaFilterUtil.getEvaluateOverTimeTypeByCriteriaFilter({
|
|
208
|
+
checkOn: CheckOn.ResponseTime,
|
|
209
|
+
filterType: FilterType.GreaterThan,
|
|
210
|
+
value: 100,
|
|
211
|
+
} as CriteriaFilter);
|
|
212
|
+
|
|
213
|
+
expect(result).toEqual([
|
|
214
|
+
EvaluateOverTimeType.Average,
|
|
215
|
+
EvaluateOverTimeType.Sum,
|
|
216
|
+
EvaluateOverTimeType.MaximumValue,
|
|
217
|
+
EvaluateOverTimeType.MunimumValue,
|
|
218
|
+
EvaluateOverTimeType.AllValues,
|
|
219
|
+
EvaluateOverTimeType.AnyValue,
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("getInverseFilterType", () => {
|
|
225
|
+
test.each([
|
|
226
|
+
[FilterType.GreaterThan, FilterType.LessThanOrEqualTo],
|
|
227
|
+
[FilterType.LessThan, FilterType.GreaterThanOrEqualTo],
|
|
228
|
+
[FilterType.GreaterThanOrEqualTo, FilterType.LessThan],
|
|
229
|
+
[FilterType.LessThanOrEqualTo, FilterType.GreaterThan],
|
|
230
|
+
[FilterType.EqualTo, FilterType.NotEqualTo],
|
|
231
|
+
[FilterType.NotEqualTo, FilterType.EqualTo],
|
|
232
|
+
])("inverts %s to %s", (input: FilterType, expected: FilterType) => {
|
|
233
|
+
expect(CriteriaFilterUtil.getInverseFilterType(input)).toBe(expected);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("returns the same filter type when there is no inverse", () => {
|
|
237
|
+
expect(CriteriaFilterUtil.getInverseFilterType(FilterType.Contains)).toBe(
|
|
238
|
+
FilterType.Contains,
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test.each([
|
|
243
|
+
FilterType.Contains,
|
|
244
|
+
FilterType.NotContains,
|
|
245
|
+
FilterType.StartsWith,
|
|
246
|
+
FilterType.EndsWith,
|
|
247
|
+
FilterType.IsEmpty,
|
|
248
|
+
FilterType.True,
|
|
249
|
+
])(
|
|
250
|
+
"returns %s itself for non-invertible filter types",
|
|
251
|
+
(filterType: FilterType) => {
|
|
252
|
+
expect(CriteriaFilterUtil.getInverseFilterType(filterType)).toBe(
|
|
253
|
+
filterType,
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
test("inversion is reversible for comparison filters", () => {
|
|
259
|
+
const inverse: FilterType = CriteriaFilterUtil.getInverseFilterType(
|
|
260
|
+
FilterType.GreaterThan,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
expect(CriteriaFilterUtil.getInverseFilterType(inverse)).toBe(
|
|
264
|
+
FilterType.GreaterThan,
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("isEvaluateOverTimeFilter", () => {
|
|
270
|
+
test.each([
|
|
271
|
+
CheckOn.ResponseStatusCode,
|
|
272
|
+
CheckOn.ResponseTime,
|
|
273
|
+
CheckOn.CPUUsagePercent,
|
|
274
|
+
CheckOn.MemoryUsagePercent,
|
|
275
|
+
CheckOn.IsOnline,
|
|
276
|
+
CheckOn.DnsResponseTime,
|
|
277
|
+
])("returns true for %s", (checkOn: CheckOn) => {
|
|
278
|
+
expect(CriteriaFilterUtil.isEvaluateOverTimeFilter(checkOn)).toBe(true);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test.each([CheckOn.ResponseBody, CheckOn.ResponseHeader, CheckOn.Error])(
|
|
282
|
+
"returns false for %s",
|
|
283
|
+
(checkOn: CheckOn) => {
|
|
284
|
+
expect(CriteriaFilterUtil.isEvaluateOverTimeFilter(checkOn)).toBe(
|
|
285
|
+
false,
|
|
286
|
+
);
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
test.each([
|
|
291
|
+
CheckOn.ResponseStatusCode,
|
|
292
|
+
CheckOn.ResponseTime,
|
|
293
|
+
CheckOn.DiskUsagePercent,
|
|
294
|
+
CheckOn.CPUUsagePercent,
|
|
295
|
+
CheckOn.MemoryUsagePercent,
|
|
296
|
+
CheckOn.LoadAverage1Min,
|
|
297
|
+
CheckOn.LoadAverage5Min,
|
|
298
|
+
CheckOn.LoadAverage15Min,
|
|
299
|
+
CheckOn.SwapUsagePercent,
|
|
300
|
+
CheckOn.CPUIoWaitPercent,
|
|
301
|
+
CheckOn.IsOnline,
|
|
302
|
+
CheckOn.SnmpResponseTime,
|
|
303
|
+
CheckOn.SnmpIsOnline,
|
|
304
|
+
CheckOn.DnsResponseTime,
|
|
305
|
+
CheckOn.DnsIsOnline,
|
|
306
|
+
CheckOn.ExternalStatusPageResponseTime,
|
|
307
|
+
CheckOn.ExternalStatusPageIsOnline,
|
|
308
|
+
])(
|
|
309
|
+
"returns true for every evaluate-over-time checkOn %s",
|
|
310
|
+
(checkOn: CheckOn) => {
|
|
311
|
+
expect(CriteriaFilterUtil.isEvaluateOverTimeFilter(checkOn)).toBe(true);
|
|
312
|
+
},
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
test.each([
|
|
316
|
+
CheckOn.ResponseBody,
|
|
317
|
+
CheckOn.IncomingRequest,
|
|
318
|
+
CheckOn.MetricValue,
|
|
319
|
+
])(
|
|
320
|
+
"returns false for non-evaluate-over-time checkOn %s",
|
|
321
|
+
(checkOn: CheckOn) => {
|
|
322
|
+
expect(CriteriaFilterUtil.isEvaluateOverTimeFilter(checkOn)).toBe(
|
|
323
|
+
false,
|
|
324
|
+
);
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
describe("getEvaluateOverTimeTypeByCriteriaFilter (non-IsOnline)", () => {
|
|
330
|
+
test("returns the full 6-element aggregation set in order for a non-IsOnline checkOn", () => {
|
|
331
|
+
const result: Array<EvaluateOverTimeType> =
|
|
332
|
+
CriteriaFilterUtil.getEvaluateOverTimeTypeByCriteriaFilter({
|
|
333
|
+
checkOn: CheckOn.CPUUsagePercent,
|
|
334
|
+
filterType: FilterType.GreaterThan,
|
|
335
|
+
value: 90,
|
|
336
|
+
} as CriteriaFilter);
|
|
337
|
+
|
|
338
|
+
expect(result).toEqual([
|
|
339
|
+
EvaluateOverTimeType.Average,
|
|
340
|
+
EvaluateOverTimeType.Sum,
|
|
341
|
+
EvaluateOverTimeType.MaximumValue,
|
|
342
|
+
EvaluateOverTimeType.MunimumValue,
|
|
343
|
+
EvaluateOverTimeType.AllValues,
|
|
344
|
+
EvaluateOverTimeType.AnyValue,
|
|
345
|
+
]);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import DayOfWeek from "../../../Types/Day/DayOfWeek";
|
|
2
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
3
|
+
import { JSONObject, ObjectType } from "../../../Types/JSON";
|
|
4
|
+
import RestrictionTimes, {
|
|
5
|
+
RestrictionType,
|
|
6
|
+
WeeklyResctriction,
|
|
7
|
+
} from "../../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
8
|
+
|
|
9
|
+
describe("RestrictionTimes", () => {
|
|
10
|
+
describe("getDefaultRestrictonTimeData / getDefault", () => {
|
|
11
|
+
test("default data has no restriction", () => {
|
|
12
|
+
const data: ReturnType<
|
|
13
|
+
typeof RestrictionTimes.getDefaultRestrictonTimeData
|
|
14
|
+
> = RestrictionTimes.getDefaultRestrictonTimeData();
|
|
15
|
+
|
|
16
|
+
expect(data.restictionType).toBe(RestrictionType.None);
|
|
17
|
+
expect(data.dayRestrictionTimes).toBeNull();
|
|
18
|
+
expect(data.weeklyRestrictionTimes).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("getDefault returns an instance with no restriction", () => {
|
|
22
|
+
const restrictionTimes: RestrictionTimes = RestrictionTimes.getDefault();
|
|
23
|
+
|
|
24
|
+
expect(restrictionTimes).toBeInstanceOf(RestrictionTimes);
|
|
25
|
+
expect(restrictionTimes.restictionType).toBe(RestrictionType.None);
|
|
26
|
+
expect(restrictionTimes.dayRestrictionTimes).toBeNull();
|
|
27
|
+
expect(restrictionTimes.weeklyRestrictionTimes).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("getDefaultRestrictonTimeData returns a fresh object each call", () => {
|
|
31
|
+
const first: ReturnType<
|
|
32
|
+
typeof RestrictionTimes.getDefaultRestrictonTimeData
|
|
33
|
+
> = RestrictionTimes.getDefaultRestrictonTimeData();
|
|
34
|
+
const second: ReturnType<
|
|
35
|
+
typeof RestrictionTimes.getDefaultRestrictonTimeData
|
|
36
|
+
> = RestrictionTimes.getDefaultRestrictonTimeData();
|
|
37
|
+
|
|
38
|
+
expect(first).not.toBe(second);
|
|
39
|
+
expect(first.weeklyRestrictionTimes).not.toBe(
|
|
40
|
+
second.weeklyRestrictionTimes,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Mutating the first call's array must not leak into the second call.
|
|
44
|
+
first.weeklyRestrictionTimes.push(
|
|
45
|
+
RestrictionTimes.getDefaultWeeklyRestrictionTIme(),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(first.weeklyRestrictionTimes.length).toBe(1);
|
|
49
|
+
expect(second.weeklyRestrictionTimes).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("getDefault returns a new instance each call", () => {
|
|
53
|
+
const first: RestrictionTimes = RestrictionTimes.getDefault();
|
|
54
|
+
const second: RestrictionTimes = RestrictionTimes.getDefault();
|
|
55
|
+
|
|
56
|
+
expect(first).not.toBe(second);
|
|
57
|
+
|
|
58
|
+
// Mutating one instance must not affect the other.
|
|
59
|
+
first.addDefaultWeeklyRestriction();
|
|
60
|
+
|
|
61
|
+
expect(first.restictionType).toBe(RestrictionType.Weekly);
|
|
62
|
+
expect(second.restictionType).toBe(RestrictionType.None);
|
|
63
|
+
expect(second.weeklyRestrictionTimes).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("getDefaultWeeklyRestrictionTIme", () => {
|
|
68
|
+
test("spans Sunday to Monday with date start/end times", () => {
|
|
69
|
+
const weekly: WeeklyResctriction =
|
|
70
|
+
RestrictionTimes.getDefaultWeeklyRestrictionTIme();
|
|
71
|
+
|
|
72
|
+
expect(weekly.startDay).toBe(DayOfWeek.Sunday);
|
|
73
|
+
expect(weekly.endDay).toBe(DayOfWeek.Monday);
|
|
74
|
+
expect(weekly.startTime).toBeInstanceOf(Date);
|
|
75
|
+
expect(weekly.endTime).toBeInstanceOf(Date);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("addDefaultDailyRestriction", () => {
|
|
80
|
+
test("sets a daily restriction with day times and clears weekly", () => {
|
|
81
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
82
|
+
restrictionTimes.addDefaultDailyRestriction();
|
|
83
|
+
|
|
84
|
+
expect(restrictionTimes.restictionType).toBe(RestrictionType.Daily);
|
|
85
|
+
expect(restrictionTimes.dayRestrictionTimes).not.toBeNull();
|
|
86
|
+
expect(restrictionTimes.dayRestrictionTimes!.startTime).toBeInstanceOf(
|
|
87
|
+
Date,
|
|
88
|
+
);
|
|
89
|
+
expect(restrictionTimes.dayRestrictionTimes!.endTime).toBeInstanceOf(
|
|
90
|
+
Date,
|
|
91
|
+
);
|
|
92
|
+
expect(restrictionTimes.weeklyRestrictionTimes).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("start time is at 00:00:00 and end time is at 01:00:00", () => {
|
|
96
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
97
|
+
restrictionTimes.addDefaultDailyRestriction();
|
|
98
|
+
|
|
99
|
+
const startTime: Date = restrictionTimes.dayRestrictionTimes!
|
|
100
|
+
.startTime as Date;
|
|
101
|
+
const endTime: Date = restrictionTimes.dayRestrictionTimes!
|
|
102
|
+
.endTime as Date;
|
|
103
|
+
|
|
104
|
+
expect(startTime.getHours()).toBe(0);
|
|
105
|
+
expect(startTime.getMinutes()).toBe(0);
|
|
106
|
+
expect(startTime.getSeconds()).toBe(0);
|
|
107
|
+
|
|
108
|
+
expect(endTime.getHours()).toBe(1);
|
|
109
|
+
expect(endTime.getMinutes()).toBe(0);
|
|
110
|
+
expect(endTime.getSeconds()).toBe(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("switching from weekly to daily clears the weekly restrictions", () => {
|
|
114
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
115
|
+
restrictionTimes.addDefaultWeeklyRestriction();
|
|
116
|
+
expect(restrictionTimes.weeklyRestrictionTimes.length).toBe(1);
|
|
117
|
+
|
|
118
|
+
restrictionTimes.addDefaultDailyRestriction();
|
|
119
|
+
|
|
120
|
+
expect(restrictionTimes.restictionType).toBe(RestrictionType.Daily);
|
|
121
|
+
expect(restrictionTimes.weeklyRestrictionTimes).toEqual([]);
|
|
122
|
+
expect(restrictionTimes.dayRestrictionTimes).not.toBeNull();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe("addDefaultWeeklyRestriction", () => {
|
|
127
|
+
test("sets a weekly restriction and clears day times", () => {
|
|
128
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
129
|
+
restrictionTimes.addDefaultWeeklyRestriction();
|
|
130
|
+
|
|
131
|
+
expect(restrictionTimes.restictionType).toBe(RestrictionType.Weekly);
|
|
132
|
+
expect(restrictionTimes.dayRestrictionTimes).toBeNull();
|
|
133
|
+
expect(restrictionTimes.weeklyRestrictionTimes.length).toBe(1);
|
|
134
|
+
expect(restrictionTimes.weeklyRestrictionTimes[0]!.startDay).toBe(
|
|
135
|
+
DayOfWeek.Sunday,
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("default weekly entry ends on Monday with date start/end times", () => {
|
|
140
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
141
|
+
restrictionTimes.addDefaultWeeklyRestriction();
|
|
142
|
+
|
|
143
|
+
const entry: WeeklyResctriction =
|
|
144
|
+
restrictionTimes.weeklyRestrictionTimes[0]!;
|
|
145
|
+
|
|
146
|
+
expect(entry.endDay).toBe(DayOfWeek.Monday);
|
|
147
|
+
expect(entry.startTime).toBeInstanceOf(Date);
|
|
148
|
+
expect(entry.endTime).toBeInstanceOf(Date);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("switching from daily to weekly clears the day restriction", () => {
|
|
152
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
153
|
+
restrictionTimes.addDefaultDailyRestriction();
|
|
154
|
+
expect(restrictionTimes.dayRestrictionTimes).not.toBeNull();
|
|
155
|
+
|
|
156
|
+
restrictionTimes.addDefaultWeeklyRestriction();
|
|
157
|
+
|
|
158
|
+
expect(restrictionTimes.restictionType).toBe(RestrictionType.Weekly);
|
|
159
|
+
expect(restrictionTimes.dayRestrictionTimes).toBeNull();
|
|
160
|
+
expect(restrictionTimes.weeklyRestrictionTimes.length).toBe(1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("removeAllRestrictions", () => {
|
|
165
|
+
test("resets a configured instance back to no restriction", () => {
|
|
166
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
167
|
+
restrictionTimes.addDefaultWeeklyRestriction();
|
|
168
|
+
|
|
169
|
+
restrictionTimes.removeAllRestrictions();
|
|
170
|
+
|
|
171
|
+
expect(restrictionTimes.restictionType).toBe(RestrictionType.None);
|
|
172
|
+
expect(restrictionTimes.dayRestrictionTimes).toBeNull();
|
|
173
|
+
expect(restrictionTimes.weeklyRestrictionTimes).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("toJSON / fromJSON", () => {
|
|
178
|
+
test("serializes with the RestrictionTimes object type", () => {
|
|
179
|
+
const json: JSONObject = new RestrictionTimes().toJSON();
|
|
180
|
+
|
|
181
|
+
expect(json["_type"]).toBe(ObjectType.RestrictionTimes);
|
|
182
|
+
expect(json["value"]).toBeDefined();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("round-trips the restriction type", () => {
|
|
186
|
+
const original: RestrictionTimes = new RestrictionTimes();
|
|
187
|
+
original.addDefaultWeeklyRestriction();
|
|
188
|
+
|
|
189
|
+
const restored: RestrictionTimes = RestrictionTimes.fromJSON(
|
|
190
|
+
original.toJSON(),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(restored).toBeInstanceOf(RestrictionTimes);
|
|
194
|
+
expect(restored.restictionType).toBe(RestrictionType.Weekly);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("round-trips a daily restriction preserving day times", () => {
|
|
198
|
+
const original: RestrictionTimes = new RestrictionTimes();
|
|
199
|
+
original.addDefaultDailyRestriction();
|
|
200
|
+
|
|
201
|
+
const restored: RestrictionTimes = RestrictionTimes.fromJSON(
|
|
202
|
+
original.toJSON(),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
expect(restored).toBeInstanceOf(RestrictionTimes);
|
|
206
|
+
expect(restored.restictionType).toBe(RestrictionType.Daily);
|
|
207
|
+
expect(restored.dayRestrictionTimes).not.toBeNull();
|
|
208
|
+
expect(restored.dayRestrictionTimes!.startTime).toBeDefined();
|
|
209
|
+
expect(restored.dayRestrictionTimes!.endTime).toBeDefined();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("falls back to an empty object when weeklyRestrictionTimes is missing", () => {
|
|
213
|
+
/*
|
|
214
|
+
* The source uses `(data["weeklyRestrictionTimes"] ...) || {}`, so when the
|
|
215
|
+
* value object omits weeklyRestrictionTimes the fallback is an empty object
|
|
216
|
+
* (NOT an empty array). This documents that quirky behavior precisely.
|
|
217
|
+
*/
|
|
218
|
+
const restored: RestrictionTimes = RestrictionTimes.fromJSON({
|
|
219
|
+
_type: ObjectType.RestrictionTimes,
|
|
220
|
+
value: {
|
|
221
|
+
restictionType: RestrictionType.None,
|
|
222
|
+
dayRestrictionTimes: null,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(restored.weeklyRestrictionTimes).toEqual({});
|
|
227
|
+
expect(Array.isArray(restored.weeklyRestrictionTimes)).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("preserves a provided weeklyRestrictionTimes array on fromJSON", () => {
|
|
231
|
+
const restored: RestrictionTimes = RestrictionTimes.fromJSON({
|
|
232
|
+
_type: ObjectType.RestrictionTimes,
|
|
233
|
+
value: {
|
|
234
|
+
restictionType: RestrictionType.None,
|
|
235
|
+
dayRestrictionTimes: null,
|
|
236
|
+
weeklyRestrictionTimes: [],
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(restored.weeklyRestrictionTimes).toEqual([]);
|
|
241
|
+
expect(Array.isArray(restored.weeklyRestrictionTimes)).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("throws BadDataException when given null", () => {
|
|
245
|
+
expect(() => {
|
|
246
|
+
RestrictionTimes.fromJSON(null as unknown as JSONObject);
|
|
247
|
+
}).toThrowError(BadDataException);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("returns the same instance when given a RestrictionTimes", () => {
|
|
251
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
252
|
+
|
|
253
|
+
expect(RestrictionTimes.fromJSON(restrictionTimes)).toBe(
|
|
254
|
+
restrictionTimes,
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("throws BadDataException for a missing object type", () => {
|
|
259
|
+
expect(() => {
|
|
260
|
+
RestrictionTimes.fromJSON({});
|
|
261
|
+
}).toThrowError(BadDataException);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("throws BadDataException when value is missing", () => {
|
|
265
|
+
expect(() => {
|
|
266
|
+
RestrictionTimes.fromJSON({ _type: ObjectType.RestrictionTimes });
|
|
267
|
+
}).toThrowError(BadDataException);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe("toDatabase / fromDatabase", () => {
|
|
272
|
+
/*
|
|
273
|
+
* toDatabase and fromDatabase are protected static overrides; reach them
|
|
274
|
+
* through a typed accessor so the database (de)serialization branches are
|
|
275
|
+
* exercised directly.
|
|
276
|
+
*/
|
|
277
|
+
type DbAccessor = {
|
|
278
|
+
toDatabase(value: unknown): JSONObject | null;
|
|
279
|
+
fromDatabase(value: JSONObject): RestrictionTimes | null;
|
|
280
|
+
};
|
|
281
|
+
const accessor: DbAccessor = RestrictionTimes as unknown as DbAccessor;
|
|
282
|
+
|
|
283
|
+
test("toDatabase serializes a RestrictionTimes instance to its JSON", () => {
|
|
284
|
+
const restrictionTimes: RestrictionTimes = new RestrictionTimes();
|
|
285
|
+
restrictionTimes.addDefaultWeeklyRestriction();
|
|
286
|
+
|
|
287
|
+
const db: JSONObject | null = accessor.toDatabase(restrictionTimes);
|
|
288
|
+
|
|
289
|
+
expect(db).not.toBeNull();
|
|
290
|
+
expect(db!["_type"]).toBe(ObjectType.RestrictionTimes);
|
|
291
|
+
expect(db).toEqual(restrictionTimes.toJSON());
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("toDatabase serializes a plain value object when not an instance", () => {
|
|
295
|
+
const plain: JSONObject = {
|
|
296
|
+
_type: ObjectType.RestrictionTimes,
|
|
297
|
+
value: {
|
|
298
|
+
restictionType: RestrictionType.None,
|
|
299
|
+
dayRestrictionTimes: null,
|
|
300
|
+
weeklyRestrictionTimes: [],
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const db: JSONObject | null = accessor.toDatabase(plain);
|
|
305
|
+
|
|
306
|
+
expect(db).not.toBeNull();
|
|
307
|
+
expect(db!["_type"]).toBe(ObjectType.RestrictionTimes);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("toDatabase returns null for a falsy value", () => {
|
|
311
|
+
expect(accessor.toDatabase(null)).toBeNull();
|
|
312
|
+
expect(accessor.toDatabase(undefined)).toBeNull();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("fromDatabase rebuilds a RestrictionTimes from stored JSON", () => {
|
|
316
|
+
const original: RestrictionTimes = new RestrictionTimes();
|
|
317
|
+
original.addDefaultWeeklyRestriction();
|
|
318
|
+
|
|
319
|
+
const restored: RestrictionTimes | null = accessor.fromDatabase(
|
|
320
|
+
original.toJSON(),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
expect(restored).toBeInstanceOf(RestrictionTimes);
|
|
324
|
+
expect(restored!.restictionType).toBe(RestrictionType.Weekly);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("fromDatabase returns null for a falsy value", () => {
|
|
328
|
+
expect(accessor.fromDatabase(null as unknown as JSONObject)).toBeNull();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
});
|