@oneuptime/common 12.0.26 → 12.0.27
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/Tests/Types/API/HostnameAuthorityParsing.test.ts +1 -0
- package/Tests/Types/API/URLMailtoQueryString.test.ts +439 -0
- package/Tests/Types/API/URLQueryString.test.ts +33 -24
- package/Tests/Types/API/URLRouteQuerySeparation.test.ts +491 -0
- package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +393 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +248 -0
- package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +205 -0
- package/Tests/UI/Components/UseHistogramZoom.test.tsx +262 -0
- package/Types/API/URL.ts +55 -18
- package/UI/Components/Charts/Utils/useHistogramZoom.ts +101 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +19 -3
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +52 -1
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +18 -2
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +52 -1
- package/build/dist/Types/API/URL.js +48 -17
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js +47 -0
- package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -3
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +33 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +14 -2
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +33 -5
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/package.json +1 -1
|
@@ -142,6 +142,7 @@ describe("URL.fromString keeps the authority separate from the rest", () => {
|
|
|
142
142
|
|
|
143
143
|
expect(url.email.toString()).toBe("support@oneuptime.com");
|
|
144
144
|
expect(url.getQueryParam("subject")).toBe("Hello");
|
|
145
|
+
expect(url.toString()).toBe("mailto:support@oneuptime.com?subject=Hello");
|
|
145
146
|
});
|
|
146
147
|
|
|
147
148
|
test("a webhook URL with basic-auth credentials still round-trips", () => {
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import Email from "../../../Types/Email";
|
|
2
|
+
import Hostname from "../../../Types/API/Hostname";
|
|
3
|
+
import Protocol from "../../../Types/API/Protocol";
|
|
4
|
+
import Route from "../../../Types/API/Route";
|
|
5
|
+
import URL from "../../../Types/API/URL";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* toString() never emitted the query string for a mailto: URL, so every
|
|
9
|
+
* prefilled field of a mail link was silently dropped:
|
|
10
|
+
* "mailto:a@b.com?subject=Hello&body=Hi" went back out as "mailto:a@b.com"
|
|
11
|
+
* and the link opened on an empty draft.
|
|
12
|
+
*
|
|
13
|
+
* The parse was never the problem — `params` held { subject, body } all along.
|
|
14
|
+
* The suffix was appended INSIDE the route-handling branch, and mailto skips
|
|
15
|
+
* that branch on purpose (it has no authority to trim and no path to append),
|
|
16
|
+
* so the query went with the route. mailto now returns early with the suffix
|
|
17
|
+
* attached, the way the opaque tel:/sms: branch above it already did.
|
|
18
|
+
*/
|
|
19
|
+
describe("mailto query string — the prefilled fields survive toString", () => {
|
|
20
|
+
test("a subject on its own reaches the output", () => {
|
|
21
|
+
const url: URL = URL.fromString("mailto:a@b.com?subject=Hello");
|
|
22
|
+
|
|
23
|
+
expect(url.getQueryParam("subject")).toBe("Hello");
|
|
24
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("a subject and a body both reach the output", () => {
|
|
28
|
+
const url: URL = URL.fromString("mailto:a@b.com?subject=Hello&body=Hi");
|
|
29
|
+
|
|
30
|
+
expect(url.params).toEqual({ subject: "Hello", body: "Hi" });
|
|
31
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello&body=Hi");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("the parse was always right — only the output was lossy", () => {
|
|
35
|
+
/*
|
|
36
|
+
* Pins the exact shape of the bug rather than just its fix: params were
|
|
37
|
+
* correct before the change and are asserted separately from toString, so
|
|
38
|
+
* a regression that breaks only one of the two still shows which one.
|
|
39
|
+
*/
|
|
40
|
+
const url: URL = URL.fromString("mailto:a@b.com?subject=Hello&body=Hi");
|
|
41
|
+
|
|
42
|
+
expect(url.params).toEqual({ subject: "Hello", body: "Hi" });
|
|
43
|
+
expect(url.toString()).not.toBe("mailto:a@b.com");
|
|
44
|
+
expect(url.toString()).toContain("subject=Hello");
|
|
45
|
+
expect(url.toString()).toContain("body=Hi");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("cc and bcc survive alongside subject and body", () => {
|
|
49
|
+
const url: URL = URL.fromString(
|
|
50
|
+
"mailto:a@b.com?cc=c@d.com&bcc=e@f.com&subject=Hello&body=Hi",
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
expect(url.params).toEqual({
|
|
54
|
+
cc: "c@d.com",
|
|
55
|
+
bcc: "e@f.com",
|
|
56
|
+
subject: "Hello",
|
|
57
|
+
body: "Hi",
|
|
58
|
+
});
|
|
59
|
+
expect(url.toString()).toBe(
|
|
60
|
+
"mailto:a@b.com?cc=c@d.com&bcc=e@f.com&subject=Hello&body=Hi",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a mailto with no query gains no stray '?'", () => {
|
|
65
|
+
const url: URL = URL.fromString("mailto:support@oneuptime.com");
|
|
66
|
+
|
|
67
|
+
expect(url.toString()).toBe("mailto:support@oneuptime.com");
|
|
68
|
+
expect(url.toString()).not.toContain("?");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("mailto query string — a value that contains '='", () => {
|
|
73
|
+
test("an '=' inside a subject is not truncated", () => {
|
|
74
|
+
const url: URL = URL.fromString("mailto:a@b.com?subject=a=b");
|
|
75
|
+
|
|
76
|
+
expect(url.getQueryParam("subject")).toBe("a=b");
|
|
77
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=a=b");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("an '=' laden body keeps its whole expression", () => {
|
|
81
|
+
const url: URL = URL.fromString(
|
|
82
|
+
"mailto:ops@example.com?subject=Alert&body=filter: status = open AND team = ops",
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
expect(url.getQueryParam("body")).toBe(
|
|
86
|
+
"filter: status = open AND team = ops",
|
|
87
|
+
);
|
|
88
|
+
expect(url.toString()).toBe(
|
|
89
|
+
"mailto:ops@example.com?subject=Alert&body=filter: status = open AND team = ops",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("only the first '=' separates, however many follow", () => {
|
|
94
|
+
const url: URL = URL.fromString("mailto:a@b.com?body=x=y=z");
|
|
95
|
+
|
|
96
|
+
expect(url.params).toEqual({ body: "x=y=z" });
|
|
97
|
+
expect(url.toString()).toBe("mailto:a@b.com?body=x=y=z");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("'==' padding does not swallow the params that follow it", () => {
|
|
101
|
+
const url: URL = URL.fromString(
|
|
102
|
+
"mailto:a@b.com?ref=YWJjZGVmZ2g==&subject=Hello",
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(url.params).toEqual({ ref: "YWJjZGVmZ2g==", subject: "Hello" });
|
|
106
|
+
expect(url.toString()).toBe(
|
|
107
|
+
"mailto:a@b.com?ref=YWJjZGVmZ2g==&subject=Hello",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("mailto query string — round trip is lossless", () => {
|
|
113
|
+
const roundTripCases: Array<string> = [
|
|
114
|
+
"mailto:a@b.com",
|
|
115
|
+
"mailto:a@b.com?subject=Hello",
|
|
116
|
+
"mailto:a@b.com?subject=Hello&body=Hi",
|
|
117
|
+
"mailto:a@b.com?body=Hi&subject=Hello",
|
|
118
|
+
"mailto:a@b.com?cc=c@d.com&bcc=e@f.com&subject=Hello&body=Hi",
|
|
119
|
+
"mailto:a@b.com?subject=a=b",
|
|
120
|
+
"mailto:a@b.com?body=x=y=z",
|
|
121
|
+
"mailto:a@b.com?ref=YWJjZGVmZ2g==&subject=Hello",
|
|
122
|
+
"mailto:a@b.com?subject=Hello%20World&body=Line%0A2",
|
|
123
|
+
"mailto:a@b.com?subject=café ✓",
|
|
124
|
+
"mailto:a@b.com?body=see?tab=1",
|
|
125
|
+
"mailto:a@b.com?body=/a/b",
|
|
126
|
+
"mailto:a@b.com?flag",
|
|
127
|
+
"mailto:a+tag@b.com?subject=Hello",
|
|
128
|
+
"mailto:first.last@sub.example.co.uk?subject=Hello",
|
|
129
|
+
"mailto:support@oneuptime.com?subject=OneUptime%20Microsoft%20Teams%20Bot",
|
|
130
|
+
"mailto:security@oneuptime.com?subject=Security%20documentation%20request",
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
test.each(roundTripCases)(
|
|
134
|
+
"URL.fromString(%s).toString() returns it unchanged",
|
|
135
|
+
(original: string) => {
|
|
136
|
+
expect(URL.fromString(original).toString()).toBe(original);
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
test.each(roundTripCases)(
|
|
141
|
+
"a second parse of %s is stable",
|
|
142
|
+
(original: string) => {
|
|
143
|
+
const once: string = URL.fromString(original).toString();
|
|
144
|
+
|
|
145
|
+
expect(URL.fromString(once).toString()).toBe(once);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
test("fromURL round trips a mailto with a query", () => {
|
|
150
|
+
const url: URL = URL.fromString("mailto:a@b.com?subject=Hello&body=Hi");
|
|
151
|
+
|
|
152
|
+
expect(URL.fromURL(url).toString()).toBe(
|
|
153
|
+
"mailto:a@b.com?subject=Hello&body=Hi",
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe("mailto query string — degenerate segments", () => {
|
|
159
|
+
test("a trailing '?' produces no params and no '?'", () => {
|
|
160
|
+
const url: URL = URL.fromString("mailto:a@b.com?");
|
|
161
|
+
|
|
162
|
+
expect(url.params).toEqual({});
|
|
163
|
+
expect(url.toString()).toBe("mailto:a@b.com");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a value-less param is kept and re-emits bare", () => {
|
|
167
|
+
const url: URL = URL.fromString("mailto:a@b.com?flag");
|
|
168
|
+
|
|
169
|
+
expect(url.params).toEqual({ flag: "" });
|
|
170
|
+
expect(url.toString()).toBe("mailto:a@b.com?flag");
|
|
171
|
+
expect(url.toString()).not.toContain("flag=");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("an explicitly empty subject re-emits bare", () => {
|
|
175
|
+
const url: URL = URL.fromString("mailto:a@b.com?subject=");
|
|
176
|
+
|
|
177
|
+
expect(url.params).toEqual({ subject: "" });
|
|
178
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("a later duplicate key wins", () => {
|
|
182
|
+
const url: URL = URL.fromString("mailto:a@b.com?subject=one&subject=two");
|
|
183
|
+
|
|
184
|
+
expect(url.getQueryParam("subject")).toBe("two");
|
|
185
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=two");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("a '?' inside a body is not truncated", () => {
|
|
189
|
+
/*
|
|
190
|
+
* The address is taken from everything before the FIRST "?", and the query
|
|
191
|
+
* is everything after it — so a body that carries its own "?" survives.
|
|
192
|
+
*/
|
|
193
|
+
const url: URL = URL.fromString("mailto:a@b.com?body=see?tab=1");
|
|
194
|
+
|
|
195
|
+
expect(url.getQueryParam("body")).toBe("see?tab=1");
|
|
196
|
+
expect(url.toString()).toBe("mailto:a@b.com?body=see?tab=1");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a '/' inside a body does not fabricate a route", () => {
|
|
200
|
+
/*
|
|
201
|
+
* Unlike a path-less https URL, whose authority is split off at the first
|
|
202
|
+
* "/" before the query is separated, a mailto never reaches that code:
|
|
203
|
+
* fromString takes its address from url.split("?")[0].
|
|
204
|
+
*/
|
|
205
|
+
const url: URL = URL.fromString("mailto:a@b.com?body=/a/b");
|
|
206
|
+
|
|
207
|
+
expect(url.getQueryParam("body")).toBe("/a/b");
|
|
208
|
+
expect(url.route.toString()).toBe("");
|
|
209
|
+
expect(url.toString()).toBe("mailto:a@b.com?body=/a/b");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("a unicode subject is passed through untouched", () => {
|
|
213
|
+
const url: URL = URL.fromString("mailto:a@b.com?subject=café ✓");
|
|
214
|
+
|
|
215
|
+
expect(url.getQueryParam("subject")).toBe("café ✓");
|
|
216
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=café ✓");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("a percent-encoded subject and body are passed through verbatim", () => {
|
|
220
|
+
const url: URL = URL.fromString(
|
|
221
|
+
"mailto:a@b.com?subject=Hello%20World&body=Line%0A2",
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(url.params).toEqual({
|
|
225
|
+
subject: "Hello%20World",
|
|
226
|
+
body: "Line%0A2",
|
|
227
|
+
});
|
|
228
|
+
expect(url.toString()).toBe(
|
|
229
|
+
"mailto:a@b.com?subject=Hello%20World&body=Line%0A2",
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("a pair with no key is dropped", () => {
|
|
234
|
+
const url: URL = URL.fromString("mailto:a@b.com?=novalue&subject=Hello");
|
|
235
|
+
|
|
236
|
+
expect(url.params).toEqual({ subject: "Hello" });
|
|
237
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("mailto query string — the address is still an Email", () => {
|
|
242
|
+
test("the email is readable with a query present", () => {
|
|
243
|
+
const url: URL = URL.fromString("mailto:support@oneuptime.com?subject=Hi");
|
|
244
|
+
|
|
245
|
+
expect(url.email.toString()).toBe("support@oneuptime.com");
|
|
246
|
+
expect(url.protocol).toBe(Protocol.MAIL);
|
|
247
|
+
expect(url.toString()).toBe("mailto:support@oneuptime.com?subject=Hi");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("a plus-addressed recipient keeps its tag", () => {
|
|
251
|
+
const url: URL = URL.fromString("mailto:a+tag@b.com?subject=Hello");
|
|
252
|
+
|
|
253
|
+
expect(url.email.toString()).toBe("a+tag@b.com");
|
|
254
|
+
expect(url.toString()).toBe("mailto:a+tag@b.com?subject=Hello");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("an upper-case scheme is normalised and keeps its query", () => {
|
|
258
|
+
const url: URL = URL.fromString("MAILTO:a@b.com?subject=Hello");
|
|
259
|
+
|
|
260
|
+
expect(url.email.toString()).toBe("a@b.com");
|
|
261
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("a mailto payload that is not an email still keeps its query", () => {
|
|
265
|
+
/*
|
|
266
|
+
* "notanemail" fails Email.isValid, so the constructor stores it as a
|
|
267
|
+
* hostname and `email` is never set. The guard has to catch this case on
|
|
268
|
+
* the "mailto:" prefix rather than on `email`, or the query would be lost
|
|
269
|
+
* again for exactly these values.
|
|
270
|
+
*/
|
|
271
|
+
const url: URL = URL.fromString("mailto:notanemail?subject=Hello");
|
|
272
|
+
|
|
273
|
+
expect(url.email).toBeUndefined();
|
|
274
|
+
expect(url.hostname.toString()).toBe("notanemail");
|
|
275
|
+
expect(url.toString()).toBe("mailto:notanemail?subject=Hello");
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("mailto query string — constructed rather than parsed", () => {
|
|
280
|
+
test("the constructor takes a query alongside an Email", () => {
|
|
281
|
+
const url: URL = new URL(
|
|
282
|
+
Protocol.MAIL,
|
|
283
|
+
new Email("a@b.com"),
|
|
284
|
+
undefined,
|
|
285
|
+
"subject=Hello&body=Hi",
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
expect(url.params).toEqual({ subject: "Hello", body: "Hi" });
|
|
289
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello&body=Hi");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("the constructor takes a query alongside a string address", () => {
|
|
293
|
+
const url: URL = new URL(
|
|
294
|
+
Protocol.MAIL,
|
|
295
|
+
"a@b.com",
|
|
296
|
+
undefined,
|
|
297
|
+
"subject=Hello",
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(url.email.toString()).toBe("a@b.com");
|
|
301
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("addQueryParam prefills a mailto that had no query", () => {
|
|
305
|
+
const url: URL = URL.fromString("mailto:a@b.com").addQueryParam(
|
|
306
|
+
"subject",
|
|
307
|
+
"Hello",
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("addQueryParam with encode still round trips", () => {
|
|
314
|
+
const url: URL = URL.fromString("mailto:a@b.com").addQueryParam(
|
|
315
|
+
"subject",
|
|
316
|
+
"Hello World",
|
|
317
|
+
true,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
expect(url.getQueryParam("subject")).toBe("Hello%20World");
|
|
321
|
+
expect(URL.fromString(url.toString()).getQueryParam("subject")).toBe(
|
|
322
|
+
"Hello%20World",
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("addQueryParams merges a body into an existing subject", () => {
|
|
327
|
+
const url: URL = URL.fromString(
|
|
328
|
+
"mailto:a@b.com?subject=Hello",
|
|
329
|
+
).addQueryParams({ body: "Hi" });
|
|
330
|
+
|
|
331
|
+
expect(url.params).toEqual({ subject: "Hello", body: "Hi" });
|
|
332
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello&body=Hi");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("removeQueryString strips the prefilled fields back off", () => {
|
|
336
|
+
const url: URL = URL.fromString(
|
|
337
|
+
"mailto:a@b.com?subject=Hello&body=Hi",
|
|
338
|
+
).removeQueryString();
|
|
339
|
+
|
|
340
|
+
expect(url.params).toEqual({});
|
|
341
|
+
expect(url.toString()).toBe("mailto:a@b.com");
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("mailto query string — crossing the wire", () => {
|
|
346
|
+
test("toJSON carries the query", () => {
|
|
347
|
+
const url: URL = URL.fromString("mailto:a@b.com?subject=Hello&body=Hi");
|
|
348
|
+
|
|
349
|
+
expect(url.toJSON()).toEqual({
|
|
350
|
+
_type: "URL",
|
|
351
|
+
value: "mailto:a@b.com?subject=Hello&body=Hi",
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("fromJSON restores a mailto with its query intact", () => {
|
|
356
|
+
const url: URL = URL.fromJSON(
|
|
357
|
+
URL.fromString("mailto:a@b.com?subject=Hello&body=Hi").toJSON(),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
expect(url.getQueryParam("subject")).toBe("Hello");
|
|
361
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello&body=Hi");
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("a mailto with a query is not treated as malformed", () => {
|
|
365
|
+
const url: URL = URL.fromStringLenient("mailto:a@b.com?subject=Hello");
|
|
366
|
+
|
|
367
|
+
expect(url.isMalformed()).toBe(false);
|
|
368
|
+
expect(url.toString()).toBe("mailto:a@b.com?subject=Hello");
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
/*
|
|
373
|
+
* The fix returns early for mailto instead of widening the branch that builds
|
|
374
|
+
* the route, so the branch itself moved. These pin the shapes that go through
|
|
375
|
+
* it — every one of them held before the change and must still hold.
|
|
376
|
+
*/
|
|
377
|
+
describe("mailto query string — every other scheme is unaffected", () => {
|
|
378
|
+
const untouchedCases: Array<string> = [
|
|
379
|
+
"https://example.com/a/b?x=1",
|
|
380
|
+
"https://example.com/path",
|
|
381
|
+
"http://localhost:5000/api/test?a=1",
|
|
382
|
+
"wss://example.com/socket?a=1",
|
|
383
|
+
"mongodb://example.com/db?a=1",
|
|
384
|
+
"https://user:token@hooks.example.com/x",
|
|
385
|
+
"https://example.com/p?flag",
|
|
386
|
+
"https://example.com/p?a=b=c=d",
|
|
387
|
+
"tel:+15555550123?ref=a=b",
|
|
388
|
+
"sms:+15555550123?body=x=1&y=2",
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
test.each(untouchedCases)("%s still round trips", (original: string) => {
|
|
392
|
+
expect(URL.fromString(original).toString()).toBe(original);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("a path-less https URL still gains its trailing slash", () => {
|
|
396
|
+
expect(URL.fromString("https://example.com").toString()).toBe(
|
|
397
|
+
"https://example.com/",
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("a route given with a leading slash is not doubled", () => {
|
|
402
|
+
const url: URL = new URL(
|
|
403
|
+
Protocol.HTTPS,
|
|
404
|
+
new Hostname("example.com"),
|
|
405
|
+
new Route("/a/b"),
|
|
406
|
+
"x=1",
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
expect(url.toString()).toBe("https://example.com/a/b?x=1");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("a route given without a leading slash still gets one", () => {
|
|
413
|
+
const url: URL = new URL(
|
|
414
|
+
Protocol.HTTPS,
|
|
415
|
+
new Hostname("example.com"),
|
|
416
|
+
new Route("a/b"),
|
|
417
|
+
"x=1",
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
expect(url.toString()).toBe("https://example.com/a/b?x=1");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("an empty route still renders as a bare slash", () => {
|
|
424
|
+
const url: URL = new URL(
|
|
425
|
+
Protocol.HTTPS,
|
|
426
|
+
new Hostname("example.com"),
|
|
427
|
+
new Route(""),
|
|
428
|
+
"x=1",
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(url.toString()).toBe("https://example.com/?x=1");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("a malformed URL is still echoed back raw", () => {
|
|
435
|
+
expect(URL.fromStringLenient("https://tel:1234567890/").toString()).toBe(
|
|
436
|
+
"https://tel:1234567890/",
|
|
437
|
+
);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
@@ -235,6 +235,9 @@ describe("URL query string — round trip is lossless", () => {
|
|
|
235
235
|
'https://example.com/p?json={"where":"a=b"}&page=2',
|
|
236
236
|
"https://example.com/p?q=a%3Db",
|
|
237
237
|
"http://localhost:5000/api/test?a=1&b=x=y",
|
|
238
|
+
"https://example.com/p?next=/a/b",
|
|
239
|
+
"https://example.com/?next=/a/b",
|
|
240
|
+
"https://example.com/?redirect=https://app.example.com/home?tab=1",
|
|
238
241
|
"tel:+15555550123?ref=a=b",
|
|
239
242
|
"sms:+15555550123?body=x=1&y=2",
|
|
240
243
|
];
|
|
@@ -400,12 +403,36 @@ describe("URL query string — constructed rather than parsed", () => {
|
|
|
400
403
|
});
|
|
401
404
|
|
|
402
405
|
/*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
406
|
+
* A "/" inside a query value used to be read as a path segment whenever the
|
|
407
|
+
* URL had no path of its own, because fromString split the authority off on
|
|
408
|
+
* the first "/" before the query was ever separated. The query is now cut off
|
|
409
|
+
* first, so no path is invented. URLRouteQuerySeparation.test.ts covers this
|
|
410
|
+
* in full; these two stay here because this is where the behaviour was pinned
|
|
411
|
+
* as a known limitation of the "=" fix.
|
|
412
|
+
*/
|
|
413
|
+
describe("URL query string — a '/' inside a value", () => {
|
|
414
|
+
test("no path is fabricated when the URL has none of its own", () => {
|
|
415
|
+
const url: URL = URL.fromString("https://example.com?next=/a/b");
|
|
416
|
+
|
|
417
|
+
expect(url.getQueryParam("next")).toBe("/a/b");
|
|
418
|
+
expect(url.route.toString()).toBe("");
|
|
419
|
+
expect(url.toString()).toBe("https://example.com/?next=/a/b");
|
|
420
|
+
expect(url.toString()).not.toBe("https://example.com/a/b?next=/a/b");
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("and a '/' in a value is harmless once the URL has a path", () => {
|
|
424
|
+
const original: string = "https://example.com/p?next=/a/b";
|
|
425
|
+
|
|
426
|
+
expect(URL.fromString(original).toString()).toBe(original);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
/*
|
|
431
|
+
* Round-tripping is lossless for the cases above, but not universally. The gap
|
|
432
|
+
* below is older than the "=" fix, predates it, and is not reachable from the
|
|
433
|
+
* Jira/OData/signed-URL shapes it set out to fix, which all carry string keys.
|
|
434
|
+
* It is pinned so the round-trip guarantee is not read as broader than it is;
|
|
435
|
+
* if it is fixed later, this test is the one that should change.
|
|
409
436
|
*/
|
|
410
437
|
describe("URL query string — known round-trip limitations", () => {
|
|
411
438
|
test("an integer-like key is hoisted to the front on re-serialization", () => {
|
|
@@ -420,24 +447,6 @@ describe("URL query string — known round-trip limitations", () => {
|
|
|
420
447
|
expect(url.params).toEqual({ b: "1", "1": "x", a: "2" });
|
|
421
448
|
expect(url.toString()).toBe("https://example.com/p?1=x&b=1&a=2");
|
|
422
449
|
});
|
|
423
|
-
|
|
424
|
-
test("a '/' in a value fabricates a path when the URL has no path", () => {
|
|
425
|
-
/*
|
|
426
|
-
* fromString splits the authority off on the first "/" before the query is
|
|
427
|
-
* ever separated, so a "/" inside a query value is read as a path segment.
|
|
428
|
-
* Only bites a URL with no path of its own; "…/p?next=/a/b" is fine.
|
|
429
|
-
*/
|
|
430
|
-
const url: URL = URL.fromString("https://example.com?next=/a/b");
|
|
431
|
-
|
|
432
|
-
expect(url.getQueryParam("next")).toBe("/a/b");
|
|
433
|
-
expect(url.toString()).toBe("https://example.com/a/b?next=/a/b");
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
test("but a '/' in a value is harmless once the URL has a path", () => {
|
|
437
|
-
const original: string = "https://example.com/p?next=/a/b";
|
|
438
|
-
|
|
439
|
-
expect(URL.fromString(original).toString()).toBe(original);
|
|
440
|
-
});
|
|
441
450
|
});
|
|
442
451
|
|
|
443
452
|
/*
|