@oneuptime/common 12.0.12 → 12.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
- package/Models/DatabaseModels/NetworkDevice.ts +64 -0
- package/Models/DatabaseModels/StatusPage.ts +59 -0
- package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
- package/Server/API/AIChatAPI.ts +55 -56
- package/Server/API/CommonAPI.ts +89 -0
- package/Server/API/MicrosoftTeamsAPI.ts +20 -9
- package/Server/API/SlackAPI.ts +13 -10
- package/Server/API/StatusPageAPI.ts +2197 -2128
- package/Server/API/TelemetryAPI.ts +72 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/UserAuthorization.ts +11 -2
- package/Server/Services/ApiKeyPermissionService.ts +116 -2
- package/Server/Services/DatabaseService.ts +17 -6
- package/Server/Services/LogAggregationService.ts +34 -0
- package/Server/Services/LogService.ts +21 -0
- package/Server/Services/NetworkSiteService.ts +12 -0
- package/Server/Services/SpanService.ts +21 -0
- package/Server/Services/TraceAggregationService.ts +15 -0
- package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
- package/Server/Utils/APIKey/AccessPermission.ts +16 -40
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
- package/Server/Utils/LogRedaction.ts +576 -0
- package/Server/Utils/Logger.ts +123 -46
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
- package/Server/Utils/Monitor/MonitorResource.ts +29 -9
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
- package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
- package/Server/Utils/StartServer.ts +30 -9
- package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
- package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
- package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
- package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
- package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
- package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
- package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
- package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
- package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
- package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
- package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
- package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
- package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
- package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
- package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
- package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
- package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
- package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
- package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
- package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
- package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
- package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
- package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
- package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
- package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
- package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
- package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
- package/Tests/UI/Components/MasterPage.test.tsx +36 -1
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
- package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
- package/Types/API/Hostname.ts +79 -0
- package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
- package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
- package/Types/Database/DatabaseProperty.ts +38 -0
- package/Types/Database/TableColumn.ts +34 -7
- package/Types/Events/Recurring.ts +16 -1
- package/Types/Monitor/MonitorStep.ts +13 -1
- package/Types/Monitor/MonitorType.ts +345 -40
- package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
- package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
- package/UI/Components/CardSelect/CardSelect.tsx +714 -105
- package/UI/Components/EmptyState/EmptyState.tsx +8 -1
- package/UI/Components/Footer/Footer.tsx +32 -6
- package/UI/Components/Forms/Fields/FormField.tsx +3 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Loader/PageLoader.tsx +29 -8
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
- package/UI/Components/MasterPage/MasterPage.tsx +24 -11
- package/Utils/Monitor/MonitorMetricType.ts +16 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
- package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
- package/Utils/Telemetry/CrossSignalScope.ts +75 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +48 -35
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +59 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +13 -10
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +745 -673
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +34 -3
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.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/UserAuthorization.js +6 -2
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +15 -6
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +2 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogService.js +16 -0
- package/build/dist/Server/Services/LogService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +12 -0
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/SpanService.js +16 -0
- package/build/dist/Server/Services/SpanService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +2 -0
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/LogRedaction.js +449 -0
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Logger.js +98 -45
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -7
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Hostname.js +63 -0
- package/build/dist/Types/API/Hostname.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
- package/build/dist/Types/Database/DatabaseProperty.js +38 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/TableColumn.js +24 -6
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +13 -1
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +9 -1
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +330 -31
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
- package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
- package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
- package/build/dist/UI/Components/Footer/Footer.js +7 -4
- package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
- package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
6
|
+
import MonitorType, {
|
|
7
|
+
MonitorTypeCategory,
|
|
8
|
+
MonitorTypeHelper,
|
|
9
|
+
} from "../../../Types/Monitor/MonitorType";
|
|
10
|
+
import CardSelect, {
|
|
11
|
+
CardSelectOption,
|
|
12
|
+
CardSelectOptionGroup,
|
|
13
|
+
} from "../../../UI/Components/CardSelect/CardSelect";
|
|
14
|
+
import MonitorTypeUtil from "../../../../App/FeatureSet/Dashboard/src/Utils/MonitorType";
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* The real catalog, driven through the real picker. The unit tests either side
|
|
18
|
+
* of this one prove the search ranks correctly and the component renders
|
|
19
|
+
* correctly; what is pinned here is that the dashboard hands the component the
|
|
20
|
+
* data it needs to do either - the adapter drops keywords silently, and every
|
|
21
|
+
* search below still "works" while finding nothing.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const categorizedOptions: Array<CardSelectOptionGroup> =
|
|
25
|
+
MonitorTypeUtil.monitorTypesAsCategorizedCardSelectOptions();
|
|
26
|
+
|
|
27
|
+
type RenderPickerFunction = (value?: string) => MockFunction;
|
|
28
|
+
|
|
29
|
+
const renderPicker: RenderPickerFunction = (value?: string): MockFunction => {
|
|
30
|
+
const onChange: MockFunction = getJestMockFunction();
|
|
31
|
+
|
|
32
|
+
render(
|
|
33
|
+
<CardSelect
|
|
34
|
+
options={categorizedOptions}
|
|
35
|
+
value={value}
|
|
36
|
+
onChange={onChange}
|
|
37
|
+
searchable={true}
|
|
38
|
+
searchPlaceholder="Search monitor types - try ping, ssl, k8s, postgres"
|
|
39
|
+
collapsibleGroups={true}
|
|
40
|
+
/>,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return onChange;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type SearchFunction = (value: string) => void;
|
|
47
|
+
|
|
48
|
+
const search: SearchFunction = (value: string): void => {
|
|
49
|
+
fireEvent.change(screen.getByTestId("card-select-search"), {
|
|
50
|
+
target: { value: value },
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type ShownTypesFunction = () => Array<string>;
|
|
55
|
+
|
|
56
|
+
const shownTypes: ShownTypesFunction = (): Array<string> => {
|
|
57
|
+
return screen.queryAllByRole("radio").map((card: HTMLElement) => {
|
|
58
|
+
return (card.getAttribute("data-testid") || "").replace(
|
|
59
|
+
"card-select-option-",
|
|
60
|
+
"",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
describe("Monitor type picker", () => {
|
|
66
|
+
describe("the options the dashboard builds", () => {
|
|
67
|
+
test("carries the keywords through from the monitor type catalog", () => {
|
|
68
|
+
for (const group of categorizedOptions) {
|
|
69
|
+
for (const option of group.options) {
|
|
70
|
+
expect({
|
|
71
|
+
monitorType: option.value,
|
|
72
|
+
hasKeywords: Boolean(option.keywords && option.keywords.length > 0),
|
|
73
|
+
}).toEqual({ monitorType: option.value, hasKeywords: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("carries keywords through the flat option list too", () => {
|
|
79
|
+
const flat: Array<CardSelectOption> =
|
|
80
|
+
MonitorTypeUtil.monitorTypesAsCardSelectOptions();
|
|
81
|
+
|
|
82
|
+
for (const option of flat) {
|
|
83
|
+
expect(option.keywords).toBeDefined();
|
|
84
|
+
expect((option.keywords || []).length).toBeGreaterThan(0);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("offers one group per category, in catalog order", () => {
|
|
89
|
+
const categories: Array<MonitorTypeCategory> =
|
|
90
|
+
MonitorTypeHelper.getMonitorTypeCategories();
|
|
91
|
+
|
|
92
|
+
expect(
|
|
93
|
+
categorizedOptions.map((group: CardSelectOptionGroup) => {
|
|
94
|
+
return group.label;
|
|
95
|
+
}),
|
|
96
|
+
).toEqual(
|
|
97
|
+
categories.map((category: MonitorTypeCategory) => {
|
|
98
|
+
return category.label;
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("what a user sees on opening the form", () => {
|
|
105
|
+
test("the everyday types are on screen without scrolling past anything", () => {
|
|
106
|
+
renderPicker();
|
|
107
|
+
|
|
108
|
+
expect(screen.getByTestId("card-select-option-Website")).toBeVisible();
|
|
109
|
+
expect(screen.getByTestId("card-select-option-API")).toBeVisible();
|
|
110
|
+
expect(screen.getByTestId("card-select-option-Ping")).toBeVisible();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* The whole point of the change. Every type used to render at once - nine
|
|
115
|
+
* headings and 29 cards, about six thousand pixels of them.
|
|
116
|
+
*/
|
|
117
|
+
test("the long tail starts folded away behind its headings", () => {
|
|
118
|
+
renderPicker();
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
screen.queryByTestId("card-select-option-Kubernetes"),
|
|
122
|
+
).not.toBeInTheDocument();
|
|
123
|
+
expect(
|
|
124
|
+
screen.queryByTestId("card-select-option-Ceph"),
|
|
125
|
+
).not.toBeInTheDocument();
|
|
126
|
+
expect(
|
|
127
|
+
screen.queryByTestId("card-select-option-Traces"),
|
|
128
|
+
).not.toBeInTheDocument();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("shows far fewer cards than the catalog holds", () => {
|
|
132
|
+
renderPicker();
|
|
133
|
+
|
|
134
|
+
const total: number = categorizedOptions.reduce(
|
|
135
|
+
(count: number, group: CardSelectOptionGroup) => {
|
|
136
|
+
return count + group.options.length;
|
|
137
|
+
},
|
|
138
|
+
0,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(shownTypes().length).toBeLessThan(total / 2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("every category is still reachable, with a count of what is inside", () => {
|
|
145
|
+
renderPicker();
|
|
146
|
+
|
|
147
|
+
for (const group of categorizedOptions) {
|
|
148
|
+
const header: HTMLElement = screen.getByTestId(
|
|
149
|
+
`card-select-group-${group.label}`,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(header).toBeVisible();
|
|
153
|
+
expect(header).toHaveTextContent(String(group.options.length));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("says how many types there are to choose from", () => {
|
|
158
|
+
renderPicker();
|
|
159
|
+
|
|
160
|
+
expect(
|
|
161
|
+
screen.getByTestId("card-select-search-summary"),
|
|
162
|
+
).toHaveTextContent("to choose from");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe("finding a type by the words a user already knows", () => {
|
|
167
|
+
test.each([
|
|
168
|
+
["k8s", MonitorType.Kubernetes],
|
|
169
|
+
["postgres", MonitorType.SQLQuery],
|
|
170
|
+
["heartbeat", MonitorType.IncomingRequest],
|
|
171
|
+
["tls", MonitorType.SSLCertificate],
|
|
172
|
+
["snmp", MonitorType.NetworkDevice],
|
|
173
|
+
["icmp", MonitorType.Ping],
|
|
174
|
+
["whois", MonitorType.Domain],
|
|
175
|
+
["prometheus", MonitorType.Metrics],
|
|
176
|
+
])("%s finds %s first", (query: string, expected: MonitorType) => {
|
|
177
|
+
renderPicker();
|
|
178
|
+
|
|
179
|
+
search(query);
|
|
180
|
+
|
|
181
|
+
expect(shownTypes()[0]).toBe(expected);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("a search reaches types that are folded away", () => {
|
|
185
|
+
renderPicker();
|
|
186
|
+
|
|
187
|
+
expect(
|
|
188
|
+
screen.queryByTestId("card-select-option-Kubernetes"),
|
|
189
|
+
).not.toBeInTheDocument();
|
|
190
|
+
|
|
191
|
+
search("k8s");
|
|
192
|
+
|
|
193
|
+
expect(screen.getByTestId("card-select-option-Kubernetes")).toBeVisible();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a search for a vendor finds the external status page monitor", () => {
|
|
197
|
+
renderPicker();
|
|
198
|
+
|
|
199
|
+
search("cloudflare");
|
|
200
|
+
|
|
201
|
+
expect(shownTypes()).toContain(MonitorType.ExternalStatusPage);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("a category name gathers its types together", () => {
|
|
205
|
+
renderPicker();
|
|
206
|
+
|
|
207
|
+
search("telemetry");
|
|
208
|
+
|
|
209
|
+
const shown: Array<string> = shownTypes();
|
|
210
|
+
|
|
211
|
+
expect(shown).toContain(MonitorType.Logs);
|
|
212
|
+
expect(shown).toContain(MonitorType.Metrics);
|
|
213
|
+
expect(shown).toContain(MonitorType.Traces);
|
|
214
|
+
expect(shown).not.toContain(MonitorType.Website);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("a word nothing uses says so instead of showing an empty grid", () => {
|
|
218
|
+
renderPicker();
|
|
219
|
+
|
|
220
|
+
search("mainframe");
|
|
221
|
+
|
|
222
|
+
expect(screen.getByTestId("card-select-no-results")).toBeVisible();
|
|
223
|
+
expect(shownTypes()).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("picking a type from the search results reports the monitor type", () => {
|
|
227
|
+
const onChange: MockFunction = renderPicker();
|
|
228
|
+
|
|
229
|
+
search("k8s");
|
|
230
|
+
fireEvent.click(screen.getByTestId("card-select-option-Kubernetes"));
|
|
231
|
+
|
|
232
|
+
expect(onChange).toHaveBeenCalledWith(MonitorType.Kubernetes);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe("coming back to a form that already has a type", () => {
|
|
237
|
+
test("shows the selection rather than hiding it behind a closed heading", () => {
|
|
238
|
+
renderPicker(MonitorType.Ceph);
|
|
239
|
+
|
|
240
|
+
expect(screen.getByTestId("card-select-option-Ceph")).toBeVisible();
|
|
241
|
+
expect(screen.getByTestId("card-select-option-Ceph")).toHaveAttribute(
|
|
242
|
+
"aria-checked",
|
|
243
|
+
"true",
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("leaves the rest folded away", () => {
|
|
248
|
+
renderPicker(MonitorType.Ceph);
|
|
249
|
+
|
|
250
|
+
expect(
|
|
251
|
+
screen.queryByTestId("card-select-option-Traces"),
|
|
252
|
+
).not.toBeInTheDocument();
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("browsing without searching", () => {
|
|
257
|
+
test("opening a category shows every type in it", () => {
|
|
258
|
+
renderPicker();
|
|
259
|
+
|
|
260
|
+
fireEvent.click(screen.getByTestId("card-select-group-Telemetry"));
|
|
261
|
+
|
|
262
|
+
expect(screen.getByTestId("card-select-option-Logs")).toBeVisible();
|
|
263
|
+
expect(screen.getByTestId("card-select-option-Metrics")).toBeVisible();
|
|
264
|
+
expect(screen.getByTestId("card-select-option-Traces")).toBeVisible();
|
|
265
|
+
expect(screen.getByTestId("card-select-option-Exceptions")).toBeVisible();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("a type can be picked by browsing, with no typing at all", () => {
|
|
269
|
+
const onChange: MockFunction = renderPicker();
|
|
270
|
+
|
|
271
|
+
fireEvent.click(screen.getByTestId("card-select-group-Infrastructure"));
|
|
272
|
+
fireEvent.click(screen.getByTestId("card-select-option-Kubernetes"));
|
|
273
|
+
|
|
274
|
+
expect(onChange).toHaveBeenCalledWith(MonitorType.Kubernetes);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("every type in the catalog can be reached by opening its category", () => {
|
|
278
|
+
renderPicker();
|
|
279
|
+
|
|
280
|
+
// Only the closed ones - clicking an open heading would fold it away.
|
|
281
|
+
for (const group of categorizedOptions) {
|
|
282
|
+
const header: HTMLElement = screen.getByTestId(
|
|
283
|
+
`card-select-group-${group.label}`,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
if (header.getAttribute("aria-expanded") === "false") {
|
|
287
|
+
fireEvent.click(header);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const group of categorizedOptions) {
|
|
292
|
+
for (const option of group.options) {
|
|
293
|
+
expect(
|
|
294
|
+
screen.getByTestId(`card-select-option-${option.value}`),
|
|
295
|
+
).toBeVisible();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The <meta name="robots"> half of the status page indexing opt-out, rendered
|
|
3
|
+
* from the real template.
|
|
4
|
+
*
|
|
5
|
+
* This is the channel that matters most: it is in the server-rendered HTML
|
|
6
|
+
* before the JS bundle loads, so it is what a crawler that does not run
|
|
7
|
+
* JavaScript sees. The X-Robots-Tag header that backs it up is asserted in
|
|
8
|
+
* App/Tests/StatusPage/SearchEngineIndexingResponses.test.ts.
|
|
9
|
+
*
|
|
10
|
+
* The template is read off disk rather than reproduced here, because the
|
|
11
|
+
* failure this guards against is someone editing index.ejs - a copy of the
|
|
12
|
+
* markup would keep passing while the shipped page lost its tag. It lives
|
|
13
|
+
* under Common/Tests because that is where the ejs types are installed;
|
|
14
|
+
* Common/Tests/App is the established home for tests that reach into App.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
18
|
+
import { describe, expect, it } from "@jest/globals";
|
|
19
|
+
import ejs from "ejs";
|
|
20
|
+
import fs from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
|
|
23
|
+
const INDEX_TEMPLATE: string = path.join(
|
|
24
|
+
__dirname,
|
|
25
|
+
"..",
|
|
26
|
+
"..",
|
|
27
|
+
"..",
|
|
28
|
+
"..",
|
|
29
|
+
"App",
|
|
30
|
+
"FeatureSet",
|
|
31
|
+
"StatusPage",
|
|
32
|
+
"views",
|
|
33
|
+
"index.ejs",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const NOINDEX_META: RegExp =
|
|
37
|
+
/<meta\s+name="robots"\s+content="noindex,\s*nofollow">/;
|
|
38
|
+
|
|
39
|
+
function renderIndexPage(variables: JSONObject): string {
|
|
40
|
+
return ejs.render(fs.readFileSync(INDEX_TEMPLATE, "utf8"), variables);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("the status page index template", () => {
|
|
44
|
+
it("tells crawlers not to index when the owner turned indexing off", () => {
|
|
45
|
+
const html: string = renderIndexPage({
|
|
46
|
+
title: "Acme Status",
|
|
47
|
+
description: "How Acme is doing.",
|
|
48
|
+
isSearchEngineIndexingEnabled: false,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(html).toMatch(NOINDEX_META);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("keeps the noindex tag inside <head>, where crawlers read it", () => {
|
|
55
|
+
const html: string = renderIndexPage({
|
|
56
|
+
title: "Acme Status",
|
|
57
|
+
isSearchEngineIndexingEnabled: false,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const head: string = html.split("</head>")[0] || "";
|
|
61
|
+
expect(head).toMatch(NOINDEX_META);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("says nothing about robots when indexing is on", () => {
|
|
65
|
+
/*
|
|
66
|
+
* Not "emits index, follow". A crawler already assumes that, and emitting
|
|
67
|
+
* it would override a noindex an operator set on a proxy in front of
|
|
68
|
+
* OneUptime - the last robots instruction a crawler reads is the one that
|
|
69
|
+
* counts.
|
|
70
|
+
*/
|
|
71
|
+
const html: string = renderIndexPage({
|
|
72
|
+
title: "Acme Status",
|
|
73
|
+
description: "How Acme is doing.",
|
|
74
|
+
isSearchEngineIndexingEnabled: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(html).not.toContain('name="robots"');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("says nothing about robots when the variable was never passed", () => {
|
|
81
|
+
/*
|
|
82
|
+
* The fallback render - a host that resolves to no status page - passes
|
|
83
|
+
* no flag at all, and must not throw on the missing variable either.
|
|
84
|
+
* That page is the generic placeholder and stays indexable, which is what
|
|
85
|
+
* shipped before this feature.
|
|
86
|
+
*/
|
|
87
|
+
const html: string = renderIndexPage({ title: "Status Page" });
|
|
88
|
+
|
|
89
|
+
expect(html).not.toContain('name="robots"');
|
|
90
|
+
expect(html).toContain("<title>Status Page</title>");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it.each([
|
|
94
|
+
["a truthy non-boolean", "no"],
|
|
95
|
+
["null", null],
|
|
96
|
+
["zero", 0],
|
|
97
|
+
])(
|
|
98
|
+
"only an explicit false suppresses indexing, not %s",
|
|
99
|
+
(_label: string, value: unknown) => {
|
|
100
|
+
/*
|
|
101
|
+
* The template compares against false rather than testing falsiness, so
|
|
102
|
+
* a value arriving in an unexpected shape leaves the page indexable
|
|
103
|
+
* instead of silently de-indexing it.
|
|
104
|
+
*/
|
|
105
|
+
const html: string = renderIndexPage({
|
|
106
|
+
title: "Acme Status",
|
|
107
|
+
isSearchEngineIndexingEnabled: value,
|
|
108
|
+
} as JSONObject);
|
|
109
|
+
|
|
110
|
+
expect(html).not.toContain('name="robots"');
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
it("still renders the rest of the head when indexing is off", () => {
|
|
115
|
+
/*
|
|
116
|
+
* The opt-out must not cost the page its title, description or favicon -
|
|
117
|
+
* a page kept out of search results is still a page people read.
|
|
118
|
+
*/
|
|
119
|
+
const html: string = renderIndexPage({
|
|
120
|
+
title: "Acme Status",
|
|
121
|
+
description: "How Acme is doing.",
|
|
122
|
+
faviconUrl: "/status-page-api/favicon/acme",
|
|
123
|
+
isSearchEngineIndexingEnabled: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(html).toContain("<title>Acme Status</title>");
|
|
127
|
+
expect(html).toContain('content="How Acme is doing."');
|
|
128
|
+
expect(html).toContain('href="/status-page-api/favicon/acme"');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
2
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
3
|
+
import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
|
|
4
|
+
import Label from "../../../Models/DatabaseModels/Label";
|
|
5
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
6
|
+
import Color from "../../../Types/Color";
|
|
7
|
+
import { JSONArray, JSONObject } from "../../../Types/JSON";
|
|
8
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
9
|
+
import { describe, expect, it } from "@jest/globals";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* toJSONObject reads table-column metadata off a cached, frozen, per-class
|
|
13
|
+
* vanilla instance instead of constructing one per row. These tests pin the
|
|
14
|
+
* contract that makes the cache safe: output is identical to a fresh
|
|
15
|
+
* construction, no state bleeds between rows or tenants, the cache is keyed
|
|
16
|
+
* on the declared modelType (not the instance's runtime class), and fromJSON
|
|
17
|
+
* still hands out a fresh mutable instance every call.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
describe("DatabaseBaseModel.toJSONObject with cached vanilla model", () => {
|
|
21
|
+
it("serializes a populated model with nested Entity and EntityArray columns", () => {
|
|
22
|
+
const severity: IncidentSeverity = new IncidentSeverity();
|
|
23
|
+
severity._id = "severity-id";
|
|
24
|
+
severity.name = "Critical";
|
|
25
|
+
|
|
26
|
+
const monitorOne: Monitor = new Monitor();
|
|
27
|
+
monitorOne._id = "monitor-one-id";
|
|
28
|
+
monitorOne.name = "Monitor One";
|
|
29
|
+
|
|
30
|
+
const monitorTwo: Monitor = new Monitor();
|
|
31
|
+
monitorTwo._id = "monitor-two-id";
|
|
32
|
+
monitorTwo.name = "Monitor Two";
|
|
33
|
+
|
|
34
|
+
const incident: Incident = new Incident();
|
|
35
|
+
incident._id = "incident-id";
|
|
36
|
+
incident.title = "Database is down";
|
|
37
|
+
incident.projectId = new ObjectID("project-one");
|
|
38
|
+
incident.incidentSeverity = severity;
|
|
39
|
+
incident.monitors = [monitorOne, monitorTwo];
|
|
40
|
+
|
|
41
|
+
const json: JSONObject = BaseModel.toJSONObject(incident, Incident);
|
|
42
|
+
|
|
43
|
+
expect(json["_id"]).toBe("incident-id");
|
|
44
|
+
expect(json["title"]).toBe("Database is down");
|
|
45
|
+
expect(json["projectId"]).toBe(incident.projectId);
|
|
46
|
+
|
|
47
|
+
// Nested Entity column is recursively serialized to a plain object.
|
|
48
|
+
const severityJson: JSONObject = json["incidentSeverity"] as JSONObject;
|
|
49
|
+
expect(severityJson).not.toBeInstanceOf(BaseModel);
|
|
50
|
+
expect(severityJson["_id"]).toBe("severity-id");
|
|
51
|
+
expect(severityJson["name"]).toBe("Critical");
|
|
52
|
+
|
|
53
|
+
// Nested EntityArray column is serialized item by item, order preserved.
|
|
54
|
+
const monitorsJson: JSONArray = json["monitors"] as JSONArray;
|
|
55
|
+
expect(Array.isArray(monitorsJson)).toBe(true);
|
|
56
|
+
expect(
|
|
57
|
+
monitorsJson.map((monitor: JSONObject | unknown) => {
|
|
58
|
+
expect(monitor).not.toBeInstanceOf(BaseModel);
|
|
59
|
+
return (monitor as JSONObject)["name"];
|
|
60
|
+
}),
|
|
61
|
+
).toEqual(["Monitor One", "Monitor Two"]);
|
|
62
|
+
|
|
63
|
+
// Unset columns are skipped entirely.
|
|
64
|
+
expect("description" in json).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("keeps each row's JSON isolated across repeated calls for the same class", () => {
|
|
68
|
+
const tenantOneProjectId: ObjectID = ObjectID.generate();
|
|
69
|
+
const tenantTwoProjectId: ObjectID = ObjectID.generate();
|
|
70
|
+
|
|
71
|
+
const tenantOneLabel: Label = new Label();
|
|
72
|
+
tenantOneLabel._id = "label-tenant-one";
|
|
73
|
+
tenantOneLabel.name = "Tenant One Label";
|
|
74
|
+
tenantOneLabel.projectId = tenantOneProjectId;
|
|
75
|
+
|
|
76
|
+
const tenantTwoLabel: Label = new Label();
|
|
77
|
+
tenantTwoLabel._id = "label-tenant-two";
|
|
78
|
+
tenantTwoLabel.name = "Tenant Two Label";
|
|
79
|
+
tenantTwoLabel.projectId = tenantTwoProjectId;
|
|
80
|
+
|
|
81
|
+
const jsonOne: JSONObject = BaseModel.toJSONObject(tenantOneLabel, Label);
|
|
82
|
+
const jsonTwo: JSONObject = BaseModel.toJSONObject(tenantTwoLabel, Label);
|
|
83
|
+
|
|
84
|
+
// Second serialization must not disturb the first result.
|
|
85
|
+
expect(jsonOne["_id"]).toBe("label-tenant-one");
|
|
86
|
+
expect(jsonOne["name"]).toBe("Tenant One Label");
|
|
87
|
+
expect(jsonOne["projectId"]).toBe(tenantOneProjectId);
|
|
88
|
+
|
|
89
|
+
expect(jsonTwo["_id"]).toBe("label-tenant-two");
|
|
90
|
+
expect(jsonTwo["name"]).toBe("Tenant Two Label");
|
|
91
|
+
expect(jsonTwo["projectId"]).toBe(tenantTwoProjectId);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("does not leak a key set on an earlier row into a later row without it", () => {
|
|
95
|
+
const labelWithColor: Label = new Label();
|
|
96
|
+
labelWithColor.name = "Colored";
|
|
97
|
+
labelWithColor.color = new Color("#112233");
|
|
98
|
+
|
|
99
|
+
const labelWithoutColor: Label = new Label();
|
|
100
|
+
labelWithoutColor.name = "Plain";
|
|
101
|
+
|
|
102
|
+
const jsonWithColor: JSONObject = BaseModel.toJSONObject(
|
|
103
|
+
labelWithColor,
|
|
104
|
+
Label,
|
|
105
|
+
);
|
|
106
|
+
const jsonWithoutColor: JSONObject = BaseModel.toJSONObject(
|
|
107
|
+
labelWithoutColor,
|
|
108
|
+
Label,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(jsonWithColor["color"]).toBe(labelWithColor.color);
|
|
112
|
+
expect("color" in jsonWithoutColor).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("serializes by the declared modelType, not the instance's runtime class", () => {
|
|
116
|
+
const label: Label = new Label();
|
|
117
|
+
label._id = "declared-type-label";
|
|
118
|
+
label.name = "Declared Type";
|
|
119
|
+
label.color = new Color("#445566");
|
|
120
|
+
label.projectId = ObjectID.generate();
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* Warm the subclass cache first so a cache wrongly keyed on
|
|
124
|
+
* model.constructor would surface Label columns below.
|
|
125
|
+
*/
|
|
126
|
+
BaseModel.toJSONObject(label, Label);
|
|
127
|
+
|
|
128
|
+
const baseJson: JSONObject = BaseModel.toJSONObject(label, BaseModel);
|
|
129
|
+
|
|
130
|
+
const baseColumns: Array<string> = new BaseModel().getTableColumns()
|
|
131
|
+
.columns;
|
|
132
|
+
for (const key of Object.keys(baseJson)) {
|
|
133
|
+
expect(baseColumns).toContain(key);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
expect(baseJson["_id"]).toBe("declared-type-label");
|
|
137
|
+
expect("color" in baseJson).toBe(false);
|
|
138
|
+
expect("projectId" in baseJson).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("toJSONObject on a fresh base model returns an empty object", () => {
|
|
142
|
+
const json: JSONObject = BaseModel.toJSONObject(new BaseModel(), BaseModel);
|
|
143
|
+
expect(json).toEqual({});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("fromJSON returns a fresh mutable instance on every call", () => {
|
|
147
|
+
const json: JSONObject = {
|
|
148
|
+
name: "oneuptime",
|
|
149
|
+
description: "a label",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const first: Label = BaseModel.fromJSON(json, Label) as Label;
|
|
153
|
+
const second: Label = BaseModel.fromJSON(json, Label) as Label;
|
|
154
|
+
|
|
155
|
+
expect(first).toBeInstanceOf(Label);
|
|
156
|
+
expect(second).toBeInstanceOf(Label);
|
|
157
|
+
expect(first).not.toBe(second);
|
|
158
|
+
|
|
159
|
+
first.name = "mutated";
|
|
160
|
+
expect(second.name).toBe("oneuptime");
|
|
161
|
+
expect(second.description).toBe("a label");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("toJSONObjectArray serializes every item correctly on the cache-hit path", () => {
|
|
165
|
+
const labels: Array<Label> = [1, 2, 3, 4].map((index: number) => {
|
|
166
|
+
const label: Label = new Label();
|
|
167
|
+
label._id = `label-${index}`;
|
|
168
|
+
label.name = `Label ${index}`;
|
|
169
|
+
return label;
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const jsonArray: JSONArray = BaseModel.toJSONObjectArray(labels, Label);
|
|
173
|
+
|
|
174
|
+
expect(jsonArray).toHaveLength(4);
|
|
175
|
+
jsonArray.forEach((item: JSONObject | unknown, index: number) => {
|
|
176
|
+
expect((item as JSONObject)["_id"]).toBe(`label-${index + 1}`);
|
|
177
|
+
expect((item as JSONObject)["name"]).toBe(`Label ${index + 1}`);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|