@oneuptime/common 12.0.9 → 12.0.11
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/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Models/DatabaseModels/RunnerJob.ts +17 -2
- package/Server/API/LlmProviderAPI.ts +8 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Services/NetworkDeviceService.ts +265 -12
- package/Server/Services/RunnerJobService.ts +24 -1
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Utils/LLM/LLMService.ts +108 -19
- package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
- package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
- package/Server/Utils/SSRFProtection.ts +5 -4
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
- package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
- package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
- package/Tests/Types/API/URLTelScheme.test.ts +261 -0
- package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
- package/Tests/UI/Components/Button.test.tsx +279 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
- package/Tests/UI/Components/Icon.test.tsx +57 -0
- package/Tests/UI/Components/List.test.tsx +20 -3
- package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
- package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/UI/Components/Pagination.test.tsx +721 -146
- package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
- package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
- package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
- package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
- package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/API/Protocol.ts +9 -0
- package/Types/API/URL.ts +286 -20
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
- package/Types/Runbook/RunbookStepType.ts +23 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
- package/UI/Components/Button/Button.tsx +14 -22
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/Dictionary/Dictionary.tsx +4 -39
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/UI/Components/Pagination/Pagination.tsx +373 -328
- package/UI/Components/Pagination/PaginationUtil.ts +263 -0
- package/UI/Components/Pagination/Screenshots/README.md +15 -0
- package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
- package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
- package/UI/Components/TextArea/TextArea.tsx +3 -0
- package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
- package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
- package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
- package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
- package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
- package/Utils/API.ts +243 -36
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +9 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +20 -2
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
- package/build/dist/Server/Utils/SSRFProtection.js +5 -4
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Types/API/Protocol.js +9 -0
- package/build/dist/Types/API/Protocol.js.map +1 -1
- package/build/dist/Types/API/URL.js +241 -18
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +10 -16
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
- package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
- package/build/dist/Utils/API.js +175 -34
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import Protocol from "../../../Types/API/Protocol";
|
|
2
|
+
import URL from "../../../Types/API/URL";
|
|
3
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* A status page went blank because ONE footer link, saved years earlier by a
|
|
8
|
+
* looser validator as "https://tel:3136361710/", no longer parsed. The value
|
|
9
|
+
* was hydrated by the column transformer during findBy, so the throw escaped
|
|
10
|
+
* the query and 400'd the whole /master-page config endpoint — the page never
|
|
11
|
+
* got its config and sat on "Loading..." forever.
|
|
12
|
+
*
|
|
13
|
+
* The rule these tests pin: reads tolerate a value that no longer validates,
|
|
14
|
+
* writes never accept one. A single bad legacy row may degrade itself and
|
|
15
|
+
* nothing else.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
interface Transformer {
|
|
19
|
+
to: (value: unknown) => unknown;
|
|
20
|
+
from: (value: unknown) => unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const transformer: Transformer = URL.getDatabaseTransformer() as Transformer;
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
* Values the old character-allowlist Hostname accepted and the current
|
|
27
|
+
* structural one does not. Every one of these can already be sitting in a
|
|
28
|
+
* customer's database.
|
|
29
|
+
*/
|
|
30
|
+
const legacyUnparseableValues: Array<string> = [
|
|
31
|
+
// The value that actually took the status page down.
|
|
32
|
+
"https://tel:3136361710/",
|
|
33
|
+
// Ports that are not ports.
|
|
34
|
+
"https://host:99999999/",
|
|
35
|
+
"https://example.com:notaport/",
|
|
36
|
+
// Hosts that are not hosts.
|
|
37
|
+
"https://-leading-dash.example.com/",
|
|
38
|
+
"https://trailing-dash-.example.com/",
|
|
39
|
+
"https:// /",
|
|
40
|
+
"https://exa mple.com/",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
describe("URL — reads tolerate a value that no longer parses", () => {
|
|
44
|
+
test.each(legacyUnparseableValues)(
|
|
45
|
+
"fromStringLenient(%s) does not throw",
|
|
46
|
+
(raw: string) => {
|
|
47
|
+
expect(() => {
|
|
48
|
+
return URL.fromStringLenient(raw);
|
|
49
|
+
}).not.toThrow();
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
test.each(legacyUnparseableValues)(
|
|
54
|
+
"fromStringLenient(%s) flags the value as malformed",
|
|
55
|
+
(raw: string) => {
|
|
56
|
+
expect(URL.fromStringLenient(raw).isMalformed()).toBe(true);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
test.each(legacyUnparseableValues)(
|
|
61
|
+
"fromStringLenient(%s) preserves the value verbatim",
|
|
62
|
+
(raw: string) => {
|
|
63
|
+
// The link still renders as exactly what it always was.
|
|
64
|
+
expect(URL.fromStringLenient(raw).toString()).toBe(raw);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
test.each(legacyUnparseableValues)(
|
|
69
|
+
"the column transformer reads %s without throwing",
|
|
70
|
+
(raw: string) => {
|
|
71
|
+
let result: URL | null = null;
|
|
72
|
+
|
|
73
|
+
expect(() => {
|
|
74
|
+
result = transformer.from(raw) as URL;
|
|
75
|
+
}).not.toThrow();
|
|
76
|
+
|
|
77
|
+
expect(result).toBeInstanceOf(URL);
|
|
78
|
+
expect((result as unknown as URL).toString()).toBe(raw);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
test("fromString stays strict — leniency is opt-in, on read paths only", () => {
|
|
83
|
+
for (const raw of legacyUnparseableValues) {
|
|
84
|
+
expect(() => {
|
|
85
|
+
return URL.fromString(raw);
|
|
86
|
+
}).toThrow(BadDataException);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("URL — a malformed value exposes no host", () => {
|
|
92
|
+
/*
|
|
93
|
+
* The security property that makes leniency safe: nothing can read a
|
|
94
|
+
* hostname off a malformed URL and turn it into a request target. The
|
|
95
|
+
* outbound guards (SSRFProtection) re-parse the raw string with the WHATWG
|
|
96
|
+
* parser and never trusted this type's parse anyway.
|
|
97
|
+
*/
|
|
98
|
+
test.each(legacyUnparseableValues)(
|
|
99
|
+
"%s yields no hostname and no opaque value",
|
|
100
|
+
(raw: string) => {
|
|
101
|
+
const url: URL = URL.fromStringLenient(raw);
|
|
102
|
+
|
|
103
|
+
expect(url.hostname).toBeUndefined();
|
|
104
|
+
expect(url.opaqueValue).toBe("");
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
test("a malformed URL reports the https default protocol", () => {
|
|
109
|
+
expect(URL.fromStringLenient("https://tel:3136361710/").protocol).toBe(
|
|
110
|
+
Protocol.HTTPS,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("removeQueryString on a malformed URL does not throw", () => {
|
|
115
|
+
const url: URL = URL.fromStringLenient("https://tel:3136361710/?a=1");
|
|
116
|
+
|
|
117
|
+
expect(() => {
|
|
118
|
+
return url.removeQueryString();
|
|
119
|
+
}).not.toThrow();
|
|
120
|
+
expect(url.removeQueryString().isMalformed()).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("URL — writes stay strict", () => {
|
|
125
|
+
test.each(legacyUnparseableValues)(
|
|
126
|
+
"the column transformer refuses to write back %s",
|
|
127
|
+
(raw: string) => {
|
|
128
|
+
const malformed: URL = URL.fromStringLenient(raw);
|
|
129
|
+
|
|
130
|
+
expect(() => {
|
|
131
|
+
return transformer.to(malformed);
|
|
132
|
+
}).toThrow(BadDataException);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
test("a client cannot invent a malformed URL and have it persisted", () => {
|
|
137
|
+
/*
|
|
138
|
+
* fromJSON is lenient so the browser can render a legacy link — but the
|
|
139
|
+
* value it produces is still refused on the way back into the database.
|
|
140
|
+
*/
|
|
141
|
+
const fromClient: URL = URL.fromJSON({
|
|
142
|
+
_type: "URL",
|
|
143
|
+
value: "https://tel:3136361710/",
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(fromClient.isMalformed()).toBe(true);
|
|
147
|
+
expect(() => {
|
|
148
|
+
return transformer.to(fromClient);
|
|
149
|
+
}).toThrow(BadDataException);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("a raw malformed string is refused on write too", () => {
|
|
153
|
+
expect(() => {
|
|
154
|
+
return transformer.to("https://tel:3136361710/");
|
|
155
|
+
}).toThrow(BadDataException);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("the write error names the offending value", () => {
|
|
159
|
+
expect(() => {
|
|
160
|
+
return transformer.to(URL.fromStringLenient("https://tel:3136361710/"));
|
|
161
|
+
}).toThrow("URL https://tel:3136361710/ is not in valid format.");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("valid URLs still write normally", () => {
|
|
165
|
+
expect(transformer.to(URL.fromString("https://example.com/privacy"))).toBe(
|
|
166
|
+
"https://example.com/privacy",
|
|
167
|
+
);
|
|
168
|
+
expect(transformer.to(URL.fromString("tel:+13136361710"))).toBe(
|
|
169
|
+
"tel:+13136361710",
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("URL — leniency does not touch valid values", () => {
|
|
175
|
+
const validValues: Array<string> = [
|
|
176
|
+
"https://example.com/privacy",
|
|
177
|
+
"https://docs.voice.izt.cloud/",
|
|
178
|
+
"http://localhost:5000/api/test",
|
|
179
|
+
"mailto:support@voice.izt.cloud",
|
|
180
|
+
"tel:+13136361710",
|
|
181
|
+
"sms:+15555550123",
|
|
182
|
+
"wss://localhost:5000/api/test",
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
test.each(validValues)("%s is not flagged malformed", (raw: string) => {
|
|
186
|
+
expect(URL.fromStringLenient(raw).isMalformed()).toBe(false);
|
|
187
|
+
expect((transformer.from(raw) as URL).isMalformed()).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test.each(validValues)("%s parses identically either way", (raw: string) => {
|
|
191
|
+
expect(URL.fromStringLenient(raw).toString()).toBe(
|
|
192
|
+
URL.fromString(raw).toString(),
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("an empty stored value is still null, not a malformed URL", () => {
|
|
197
|
+
expect(transformer.from("")).toBeNull();
|
|
198
|
+
expect(transformer.from(null)).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("undefined is still passed through untouched on write", () => {
|
|
202
|
+
// A column the caller never set must stay undefined so its DEFAULT applies.
|
|
203
|
+
expect(transformer.to(undefined)).toBeUndefined();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
/*
|
|
208
|
+
* The end-to-end shape of the outage: a status page's footer links, one of
|
|
209
|
+
* which is the bad legacy row. Reading the set must yield every link.
|
|
210
|
+
*/
|
|
211
|
+
describe("URL — one bad link no longer takes down the page that lists it", () => {
|
|
212
|
+
const storedFooterLinks: Array<{ title: string; link: string }> = [
|
|
213
|
+
{ title: "Phone", link: "https://tel:3136361710/" },
|
|
214
|
+
{ title: "Email", link: "mailto:support@voice.izt.cloud" },
|
|
215
|
+
{ title: "Docs", link: "https://docs.voice.izt.cloud/" },
|
|
216
|
+
{ title: "Portal", link: "https://portal.voice.izt.cloud/" },
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
test("hydrating the whole set does not throw", () => {
|
|
220
|
+
expect(() => {
|
|
221
|
+
return storedFooterLinks.map((row: { link: string }) => {
|
|
222
|
+
return transformer.from(row.link);
|
|
223
|
+
});
|
|
224
|
+
}).not.toThrow();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("every link survives, including the malformed one", () => {
|
|
228
|
+
const hydrated: Array<URL> = storedFooterLinks.map(
|
|
229
|
+
(row: { link: string }) => {
|
|
230
|
+
return transformer.from(row.link) as URL;
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
expect(hydrated).toHaveLength(4);
|
|
235
|
+
expect(
|
|
236
|
+
hydrated.map((url: URL) => {
|
|
237
|
+
return url.toString();
|
|
238
|
+
}),
|
|
239
|
+
).toEqual([
|
|
240
|
+
"https://tel:3136361710/",
|
|
241
|
+
"mailto:support@voice.izt.cloud",
|
|
242
|
+
"https://docs.voice.izt.cloud/",
|
|
243
|
+
"https://portal.voice.izt.cloud/",
|
|
244
|
+
]);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("only the bad row is degraded", () => {
|
|
248
|
+
const hydrated: Array<URL> = storedFooterLinks.map(
|
|
249
|
+
(row: { link: string }) => {
|
|
250
|
+
return transformer.from(row.link) as URL;
|
|
251
|
+
},
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(
|
|
255
|
+
hydrated.map((url: URL) => {
|
|
256
|
+
return url.isMalformed();
|
|
257
|
+
}),
|
|
258
|
+
).toEqual([true, false, false, false]);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("the whole set still serialises to JSON for the browser", () => {
|
|
262
|
+
expect(() => {
|
|
263
|
+
return storedFooterLinks.map((row: { link: string }) => {
|
|
264
|
+
return (transformer.from(row.link) as URL).toJSON();
|
|
265
|
+
});
|
|
266
|
+
}).not.toThrow();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* Had the fix stopped at the server, the 400 would simply have become a
|
|
271
|
+
* client-side crash when the browser revived the same value.
|
|
272
|
+
*/
|
|
273
|
+
test("and the browser can revive every one of them", () => {
|
|
274
|
+
const revived: Array<URL> = storedFooterLinks.map(
|
|
275
|
+
(row: { link: string }) => {
|
|
276
|
+
return URL.fromJSON((transformer.from(row.link) as URL).toJSON());
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
expect(
|
|
281
|
+
revived.map((url: URL) => {
|
|
282
|
+
return url.toString();
|
|
283
|
+
}),
|
|
284
|
+
).toEqual([
|
|
285
|
+
"https://tel:3136361710/",
|
|
286
|
+
"mailto:support@voice.izt.cloud",
|
|
287
|
+
"https://docs.voice.izt.cloud/",
|
|
288
|
+
"https://portal.voice.izt.cloud/",
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
/*
|
|
293
|
+
* And the correct value going forward: the same phone link, typed as a real
|
|
294
|
+
* tel: URI, is valid on write and needs no leniency at all.
|
|
295
|
+
*/
|
|
296
|
+
test("the same link stored correctly needs no leniency", () => {
|
|
297
|
+
const stored: unknown = transformer.to(URL.fromString("tel:+13136361710"));
|
|
298
|
+
expect(stored).toBe("tel:+13136361710");
|
|
299
|
+
expect((transformer.from(stored) as URL).isMalformed()).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import Protocol from "../../../Types/API/Protocol";
|
|
2
|
+
import URL from "../../../Types/API/URL";
|
|
3
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* "tel:" and "sms:" are opaque schemes: what follows the colon is a phone
|
|
8
|
+
* number, not an authority. Before they were known to the parser, a status
|
|
9
|
+
* page footer link of "tel:3136361710" fell through to the https default,
|
|
10
|
+
* was read as host "tel" + port "3136361710", and toString() normalised it
|
|
11
|
+
* on save into "https://tel:3136361710/". These tests pin the parse, the
|
|
12
|
+
* round-trip, and the rule that nothing in an opaque payload may look like an
|
|
13
|
+
* authority or a path.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
describe("URL — tel: and sms: are parsed as opaque schemes", () => {
|
|
17
|
+
const validTelUrls: Array<[string, string]> = [
|
|
18
|
+
// [input, expected opaque value]
|
|
19
|
+
["tel:+13136361710", "+13136361710"],
|
|
20
|
+
["tel:3136361710", "3136361710"],
|
|
21
|
+
["tel:+1-313-636-1710", "+1-313-636-1710"],
|
|
22
|
+
["tel:+1.313.636.1710", "+1.313.636.1710"],
|
|
23
|
+
// RFC 3966 allows a number to open on a visual separator.
|
|
24
|
+
["tel:(313)636-1710", "(313)636-1710"],
|
|
25
|
+
["tel:+1(313)636-1710", "+1(313)636-1710"],
|
|
26
|
+
["tel:-5550123", "-5550123"],
|
|
27
|
+
["tel:911", "911"],
|
|
28
|
+
["tel:+442071838750", "+442071838750"],
|
|
29
|
+
// RFC 3966 parameters.
|
|
30
|
+
["tel:3136361710;phone-context=+1", "3136361710;phone-context=+1"],
|
|
31
|
+
["tel:+13136361710;ext=42", "+13136361710;ext=42"],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
test.each(validTelUrls)(
|
|
35
|
+
"parses %s as a tel: URL with no hostname",
|
|
36
|
+
(input: string, expectedValue: string) => {
|
|
37
|
+
const url: URL = URL.fromString(input);
|
|
38
|
+
|
|
39
|
+
expect(url.protocol).toBe(Protocol.TEL);
|
|
40
|
+
expect(url.opaqueValue).toBe(expectedValue);
|
|
41
|
+
expect(url.isMalformed()).toBe(false);
|
|
42
|
+
// An opaque URL has no authority — nothing can read a host off it.
|
|
43
|
+
expect(url.hostname).toBeUndefined();
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
test.each(validTelUrls)(
|
|
48
|
+
"round-trips %s unchanged through toString()",
|
|
49
|
+
(input: string) => {
|
|
50
|
+
expect(URL.fromString(input).toString()).toBe(input);
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
test("parses sms: the same way", () => {
|
|
55
|
+
const url: URL = URL.fromString("sms:+15555550123");
|
|
56
|
+
|
|
57
|
+
expect(url.protocol).toBe(Protocol.SMS);
|
|
58
|
+
expect(url.opaqueValue).toBe("+15555550123");
|
|
59
|
+
expect(url.toString()).toBe("sms:+15555550123");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("sms: accepts several comma-separated recipients", () => {
|
|
63
|
+
const url: URL = URL.fromString("sms:+15555550123,+15555550124");
|
|
64
|
+
|
|
65
|
+
expect(url.opaqueValue).toBe("+15555550123,+15555550124");
|
|
66
|
+
expect(url.toString()).toBe("sms:+15555550123,+15555550124");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("sms: keeps a body query string", () => {
|
|
70
|
+
const url: URL = URL.fromString("sms:+15555550123?body=hello");
|
|
71
|
+
|
|
72
|
+
expect(url.protocol).toBe(Protocol.SMS);
|
|
73
|
+
expect(url.opaqueValue).toBe("+15555550123");
|
|
74
|
+
expect(url.getQueryParam("body")).toBe("hello");
|
|
75
|
+
expect(url.toString()).toBe("sms:+15555550123?body=hello");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("scheme match is case-insensitive, per RFC 3986", () => {
|
|
79
|
+
expect(URL.fromString("TEL:+13136361710").protocol).toBe(Protocol.TEL);
|
|
80
|
+
expect(URL.fromString("Tel:+13136361710").opaqueValue).toBe("+13136361710");
|
|
81
|
+
expect(URL.fromString("SMS:+15555550123").protocol).toBe(Protocol.SMS);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("isOpaqueProtocol identifies exactly tel: and sms:", () => {
|
|
85
|
+
expect(URL.isOpaqueProtocol(Protocol.TEL)).toBe(true);
|
|
86
|
+
expect(URL.isOpaqueProtocol(Protocol.SMS)).toBe(true);
|
|
87
|
+
expect(URL.isOpaqueProtocol(Protocol.HTTPS)).toBe(false);
|
|
88
|
+
expect(URL.isOpaqueProtocol(Protocol.HTTP)).toBe(false);
|
|
89
|
+
expect(URL.isOpaqueProtocol(Protocol.MAIL)).toBe(false);
|
|
90
|
+
expect(URL.isOpaqueProtocol(Protocol.WS)).toBe(false);
|
|
91
|
+
expect(URL.isOpaqueProtocol(Protocol.WSS)).toBe(false);
|
|
92
|
+
expect(URL.isOpaqueProtocol(Protocol.MONGO_DB)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("the constructor also takes the opaque path", () => {
|
|
96
|
+
const url: URL = new URL(Protocol.TEL, "+13136361710");
|
|
97
|
+
|
|
98
|
+
expect(url.opaqueValue).toBe("+13136361710");
|
|
99
|
+
expect(url.hostname).toBeUndefined();
|
|
100
|
+
expect(url.toString()).toBe("tel:+13136361710");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("isHttps() is false for an opaque URL", () => {
|
|
104
|
+
expect(URL.fromString("tel:+13136361710").isHttps()).toBe(false);
|
|
105
|
+
expect(URL.fromString("sms:+15555550123").isHttps()).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/*
|
|
110
|
+
* The regression this whole change exists for: the value a customer typed
|
|
111
|
+
* must survive a save/read cycle instead of being rewritten into a bogus
|
|
112
|
+
* https URL that a later, stricter Hostname validator then refuses to read.
|
|
113
|
+
*/
|
|
114
|
+
describe("URL — tel: is no longer mangled into an https URL", () => {
|
|
115
|
+
test("tel:3136361710 does not become https://tel:3136361710/", () => {
|
|
116
|
+
const url: URL = URL.fromString("tel:3136361710");
|
|
117
|
+
|
|
118
|
+
expect(url.toString()).toBe("tel:3136361710");
|
|
119
|
+
expect(url.toString()).not.toBe("https://tel:3136361710/");
|
|
120
|
+
expect(url.protocol).not.toBe(Protocol.HTTPS);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("the value survives the database round-trip", () => {
|
|
124
|
+
const transformer: {
|
|
125
|
+
to: (value: unknown) => unknown;
|
|
126
|
+
from: (value: unknown) => unknown;
|
|
127
|
+
} = URL.getDatabaseTransformer() as {
|
|
128
|
+
to: (value: unknown) => unknown;
|
|
129
|
+
from: (value: unknown) => unknown;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const stored: unknown = transformer.to(URL.fromString("tel:3136361710"));
|
|
133
|
+
expect(stored).toBe("tel:3136361710");
|
|
134
|
+
|
|
135
|
+
const readBack: URL = transformer.from(stored) as URL;
|
|
136
|
+
expect(readBack.toString()).toBe("tel:3136361710");
|
|
137
|
+
expect(readBack.isMalformed()).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("the value survives the JSON round-trip to the browser", () => {
|
|
141
|
+
const url: URL = URL.fromString("tel:+13136361710");
|
|
142
|
+
const revived: URL = URL.fromJSON(url.toJSON());
|
|
143
|
+
|
|
144
|
+
expect(revived.toString()).toBe("tel:+13136361710");
|
|
145
|
+
expect(revived.protocol).toBe(Protocol.TEL);
|
|
146
|
+
expect(revived.isMalformed()).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("URL — an opaque payload may not smuggle an authority or path", () => {
|
|
151
|
+
const invalidOpaquePayloads: Array<string> = [
|
|
152
|
+
// Empty.
|
|
153
|
+
"tel:",
|
|
154
|
+
"sms:",
|
|
155
|
+
// Not a number at all.
|
|
156
|
+
"tel:abc",
|
|
157
|
+
"tel:not-a-phone",
|
|
158
|
+
// Path traversal / a path of any kind.
|
|
159
|
+
"tel:../../etc/passwd",
|
|
160
|
+
"tel:1/../../admin",
|
|
161
|
+
"tel:+1555/path",
|
|
162
|
+
// An authority smuggled behind the number.
|
|
163
|
+
"tel:1@evil.example.com",
|
|
164
|
+
"tel:+15555550123@169.254.169.254",
|
|
165
|
+
"sms:1@evil.example.com",
|
|
166
|
+
// A fragment.
|
|
167
|
+
"tel:1#fragment",
|
|
168
|
+
"tel:+15555550123#.office.com",
|
|
169
|
+
// Whitespace.
|
|
170
|
+
"tel:555 0123",
|
|
171
|
+
"tel: ",
|
|
172
|
+
// Separators only, with no digit anywhere.
|
|
173
|
+
"tel:(",
|
|
174
|
+
"tel:()-.",
|
|
175
|
+
"tel:+",
|
|
176
|
+
// Protocol-relative smuggling.
|
|
177
|
+
"tel://evil.example.com",
|
|
178
|
+
"sms://evil.example.com",
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
test.each(invalidOpaquePayloads)("rejects %s", (input: string) => {
|
|
182
|
+
expect(() => {
|
|
183
|
+
return URL.fromString(input);
|
|
184
|
+
}).toThrow(BadDataException);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("rejects a payload longer than 256 characters", () => {
|
|
188
|
+
expect(() => {
|
|
189
|
+
return URL.fromString("tel:+" + "1".repeat(300));
|
|
190
|
+
}).toThrow(BadDataException);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("rejects a parameter that is not a well-formed key=value", () => {
|
|
194
|
+
expect(() => {
|
|
195
|
+
return URL.fromString("tel:+15555550123;evil=a/b");
|
|
196
|
+
}).toThrow(BadDataException);
|
|
197
|
+
expect(() => {
|
|
198
|
+
return URL.fromString("tel:+15555550123;a@b");
|
|
199
|
+
}).toThrow(BadDataException);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("the error names the offending value", () => {
|
|
203
|
+
expect(() => {
|
|
204
|
+
return URL.fromString("tel:abc");
|
|
205
|
+
}).toThrow("Phone number abc is not in valid format.");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* A dotted quad is a syntactically valid local number, so it parses — but it
|
|
210
|
+
* stays a tel: URL with no hostname, so it can never become the target of an
|
|
211
|
+
* HTTP request the way "https://169.254.169.254" would.
|
|
212
|
+
*/
|
|
213
|
+
test("a dotted quad stays a tel: URL and exposes no host", () => {
|
|
214
|
+
const url: URL = URL.fromString("tel:169.254.169.254");
|
|
215
|
+
|
|
216
|
+
expect(url.protocol).toBe(Protocol.TEL);
|
|
217
|
+
expect(url.hostname).toBeUndefined();
|
|
218
|
+
expect(url.toString()).toBe("tel:169.254.169.254");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe("URL — adding opaque schemes did not disturb the existing ones", () => {
|
|
223
|
+
const untouched: Array<[string, Protocol]> = [
|
|
224
|
+
["https://example.com/api/test", Protocol.HTTPS],
|
|
225
|
+
["http://example.com/api/test", Protocol.HTTP],
|
|
226
|
+
["ws://localhost:5000/api/test", Protocol.WS],
|
|
227
|
+
["wss://localhost:5000/api/test", Protocol.WSS],
|
|
228
|
+
["mongodb://localhost:27017/test", Protocol.MONGO_DB],
|
|
229
|
+
["mailto:support@example.com", Protocol.MAIL],
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
test.each(untouched)(
|
|
233
|
+
"%s still parses as %s",
|
|
234
|
+
(input: string, expected: Protocol) => {
|
|
235
|
+
const url: URL = URL.fromString(input);
|
|
236
|
+
expect(url.protocol).toBe(expected);
|
|
237
|
+
expect(url.opaqueValue).toBe("");
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
test.each(untouched)("%s still round-trips", (input: string) => {
|
|
242
|
+
expect(URL.fromString(input).toString()).toBe(input);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("query strings on http URLs are unaffected", () => {
|
|
246
|
+
const url: URL = URL.fromString("https://example.com/api?a=1&b=2");
|
|
247
|
+
expect(url.getQueryParam("a")).toBe("1");
|
|
248
|
+
expect(url.getQueryParam("b")).toBe("2");
|
|
249
|
+
expect(url.toString()).toBe("https://example.com/api?a=1&b=2");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
/*
|
|
253
|
+
* "telephone.example.com" starts with "tel" but is not the tel: scheme —
|
|
254
|
+
* prefix matching must be on "tel:", not "tel".
|
|
255
|
+
*/
|
|
256
|
+
test("a host that merely starts with tel is not treated as a tel: URL", () => {
|
|
257
|
+
const url: URL = URL.fromString("https://telephone.example.com/x");
|
|
258
|
+
expect(url.protocol).toBe(Protocol.HTTPS);
|
|
259
|
+
expect(url.hostname.toString()).toBe("telephone.example.com");
|
|
260
|
+
});
|
|
261
|
+
});
|