Skip to content

Commit 4b89fce

Browse files
github-actions[bot]CopilotmarcelofukumotoCopilotnwmac
authored
[Test Improver] test: add unit tests for shell/utils/url.ts (#17176)
* test: add unit tests for shell/utils/url.ts Add comprehensive unit tests for the URL utility functions: - addParam: single value, array values, special chars, null value - addParams: multiple params, empty/null/non-object params - removeParam: removes existing param, no-op for missing param - parseLinkHeader: single/multiple entries, empty string, malformed, rel casing - portMatch: equals list, endsWith suffix, edge cases - isMaybeSecure: https protocol, port 443/8443, http non-secure - parse: simple URL, port, credentials, anchor, multiple query params - stringify: reconstructs URL with user/password, port, anchor, default path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improvements on the tests * test: address URL utility review feedback Agent-Logs-Url: https://github.com/rancher/dashboard/sessions/213d9866-9376-4dea-85e4-bd6b35b42484 Co-authored-by: nwmac <1955897+nwmac@users.noreply.github.com> * refactor: clean up URL param typing assertions Agent-Logs-Url: https://github.com/rancher/dashboard/sessions/213d9866-9376-4dea-85e4-bd6b35b42484 Co-authored-by: nwmac <1955897+nwmac@users.noreply.github.com> * Revert changes to the types by AI * Fix unit tests --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nwmac <1955897+nwmac@users.noreply.github.com> Co-authored-by: Neil MacDougall <nmacdougall@suse.com>
1 parent d0ad6d3 commit 4b89fce

1 file changed

Lines changed: 246 additions & 0 deletions

File tree

shell/utils/__tests__/url.test.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import {
2+
addParam, addParams, removeParam, parseLinkHeader, isMaybeSecure, portMatch, parse, stringify
3+
} from '@shell/utils/url';
4+
5+
describe('fx: addParam', () => {
6+
it('should add a query parameter to a URL without existing params', () => {
7+
expect(addParam('https://example.com/path', 'foo', 'bar')).toStrictEqual('https://example.com/path?foo=bar');
8+
});
9+
10+
it('should append a query parameter to a URL with existing params', () => {
11+
expect(addParam('https://example.com/path?a=1', 'b', '2')).toStrictEqual('https://example.com/path?a=1&b=2');
12+
});
13+
14+
it('should encode special characters in key and value', () => {
15+
expect(addParam('https://example.com', 'my key', 'hello world')).toStrictEqual('https://example.com?my%20key=hello%20world');
16+
});
17+
18+
it('should add multiple values from an array', () => {
19+
expect(addParam('https://example.com', 'tag', ['a', 'b'])).toStrictEqual('https://example.com?tag=a&tag=b');
20+
});
21+
22+
it('should add a key-only param when value is null', () => {
23+
expect(addParam('https://example.com', 'flag', null)).toStrictEqual('https://example.com?flag');
24+
});
25+
26+
it('should handle an array with a null value', () => {
27+
expect(addParam('https://example.com', 'flag', [null])).toStrictEqual('https://example.com?flag');
28+
});
29+
30+
it('should add a param with an empty string value', () => {
31+
expect(addParam('https://example.com', 'key', '')).toStrictEqual('https://example.com?key=');
32+
});
33+
34+
it('should add a duplicate key as an additional param', () => {
35+
expect(addParam('https://example.com?a=1', 'a', '2')).toStrictEqual('https://example.com?a=1&a=2');
36+
});
37+
});
38+
39+
describe('fx: addParams', () => {
40+
it('should add multiple parameters to a URL', () => {
41+
expect(addParams('https://example.com', { a: '1', b: '2' })).toStrictEqual('https://example.com?a=1&b=2');
42+
});
43+
44+
it('should return the URL unchanged if params is empty', () => {
45+
expect(addParams('https://example.com', {})).toStrictEqual('https://example.com');
46+
});
47+
48+
it('should return the URL unchanged if params is null', () => {
49+
expect(addParams('https://example.com', null)).toStrictEqual('https://example.com');
50+
});
51+
52+
it('should return the URL unchanged if params is a non-object value', () => {
53+
expect(addParams('https://example.com', 'not-an-object')).toStrictEqual('https://example.com');
54+
});
55+
});
56+
57+
describe('fx: removeParam', () => {
58+
it('should remove a query parameter from a URL', () => {
59+
expect(removeParam('https://example.com?foo=bar&baz=qux', 'foo')).toStrictEqual('https://example.com/?baz=qux');
60+
});
61+
62+
it('should return a normalized URL if the param does not exist', () => {
63+
expect(removeParam('https://example.com?a=1', 'nonexistent')).toStrictEqual('https://example.com/?a=1');
64+
});
65+
66+
it('should remove the only query parameter', () => {
67+
expect(removeParam('https://example.com?only=param', 'only')).toStrictEqual('https://example.com/');
68+
});
69+
70+
it('should normalize a key-only query parameter to key= (parser treats it as empty value)', () => {
71+
expect(removeParam('https://example.com?flag', 'flag')).toStrictEqual('https://example.com/?flag=');
72+
});
73+
});
74+
75+
describe('fx: parseLinkHeader', () => {
76+
it('should parse a single link header entry', () => {
77+
expect(parseLinkHeader('<https://example.com/page2>; rel="next"')).toStrictEqual({ next: 'https://example.com/page2' });
78+
});
79+
80+
it('should parse multiple link header entries', () => {
81+
const header = '<https://example.com/page2>; rel="next", <https://example.com/page1>; rel="prev"';
82+
83+
expect(parseLinkHeader(header)).toStrictEqual({
84+
next: 'https://example.com/page2',
85+
prev: 'https://example.com/page1',
86+
});
87+
});
88+
89+
it('should return an empty object for an empty string', () => {
90+
expect(parseLinkHeader('')).toStrictEqual({});
91+
});
92+
93+
it('should return an empty object for a malformed header', () => {
94+
expect(parseLinkHeader('not a valid link header')).toStrictEqual({});
95+
});
96+
97+
it('should lowercase the rel value', () => {
98+
expect(parseLinkHeader('<https://example.com>; rel="Next"')).toStrictEqual({ next: 'https://example.com' });
99+
});
100+
});
101+
102+
describe('fx: portMatch', () => {
103+
it.each([
104+
{
105+
ports: [443], equals: [443, 8443], endsWith: [], expected: true, desc: 'port is in the equals list'
106+
},
107+
{
108+
ports: [8080], equals: [443, 8443], endsWith: ['443'], expected: false, desc: 'port is not in equals or endsWith lists'
109+
},
110+
{
111+
ports: [8443], equals: [], endsWith: ['443'], expected: true, desc: 'port string ends with the given suffix'
112+
},
113+
{
114+
ports: [443], equals: [], endsWith: ['443'], expected: false, desc: 'port equals the suffix exactly (endsWith excludes exact match)'
115+
},
116+
{
117+
ports: [], equals: [443], endsWith: ['443'], expected: false, desc: 'ports array is empty'
118+
},
119+
{
120+
ports: [80, 443], equals: [443], endsWith: [], expected: true, desc: 'any port in the array matches equals'
121+
},
122+
{
123+
ports: [18443], equals: [], endsWith: ['443'], expected: true, desc: 'multi-digit port ending with suffix'
124+
},
125+
])('should return $expected when $desc', ({
126+
ports, equals, endsWith, expected
127+
}) => {
128+
expect(portMatch(ports, equals, endsWith)).toBe(expected);
129+
});
130+
});
131+
132+
describe('fx: isMaybeSecure', () => {
133+
it.each([
134+
{
135+
port: 80, proto: 'https', expected: true, desc: 'https protocol'
136+
},
137+
{
138+
port: 80, proto: 'HTTPS', expected: true, desc: 'HTTPS protocol (case-insensitive)'
139+
},
140+
{
141+
port: 443, proto: 'http', expected: true, desc: 'port 443'
142+
},
143+
{
144+
port: 8443, proto: 'http', expected: true, desc: 'port 8443'
145+
},
146+
{
147+
port: 18443, proto: 'http', expected: true, desc: 'port 18443 (endsWith 443)'
148+
},
149+
{
150+
port: 80, proto: 'http', expected: false, desc: 'http on non-secure port'
151+
},
152+
])('should return $expected for $desc', ({ port, proto, expected }) => {
153+
expect(isMaybeSecure(port, proto)).toBe(expected);
154+
});
155+
});
156+
157+
describe('fx: parse', () => {
158+
it('should parse a simple URL', () => {
159+
const result = parse('https://example.com/path?foo=bar');
160+
161+
expect(result.protocol).toStrictEqual('https');
162+
expect(result.host).toStrictEqual('example.com');
163+
expect(result.path).toStrictEqual('/path');
164+
expect(result.query).toStrictEqual({ foo: 'bar' });
165+
});
166+
167+
it('should parse a URL with port', () => {
168+
const result = parse('https://example.com:8080/');
169+
170+
expect(result.host).toStrictEqual('example.com');
171+
expect(result.port).toStrictEqual('8080');
172+
});
173+
174+
it('should parse a URL with user credentials', () => {
175+
const result = parse('https://user:pass@example.com/');
176+
177+
expect(result.user).toStrictEqual('user');
178+
expect(result.password).toStrictEqual('pass');
179+
});
180+
181+
it('should parse a URL with anchor', () => {
182+
const result = parse('https://example.com/page#section1');
183+
184+
expect(result.anchor).toStrictEqual('section1');
185+
});
186+
187+
it('should parse a URL with multiple query params', () => {
188+
expect(parse('https://example.com?a=1&b=2').query).toStrictEqual({ a: '1', b: '2' });
189+
});
190+
191+
it('should parse a URL with user only (no password)', () => {
192+
const result = parse('https://admin@example.com/');
193+
194+
expect(result.user).toStrictEqual('admin');
195+
expect(result.password).toStrictEqual('');
196+
});
197+
198+
it('should set empty strings for missing optional fields', () => {
199+
const result = parse('https://example.com/path');
200+
201+
expect(result.port).toStrictEqual('');
202+
expect(result.anchor).toStrictEqual('');
203+
expect(result.user).toStrictEqual('');
204+
expect(result.password).toStrictEqual('');
205+
});
206+
});
207+
208+
describe('fx: stringify', () => {
209+
it('should reconstruct a simple URL', () => {
210+
expect(stringify(parse('https://example.com/path'))).toStrictEqual('https://example.com/path');
211+
});
212+
213+
it('should include user and password when both present', () => {
214+
expect(stringify(parse('https://user:pass@example.com/'))).toStrictEqual('https://user:pass@example.com/');
215+
});
216+
217+
it('should include user only when password is absent', () => {
218+
expect(stringify(parse('https://admin@example.com/'))).toStrictEqual('https://admin@example.com/');
219+
});
220+
221+
it('should include port when present', () => {
222+
expect(stringify(parse('https://example.com:9090/'))).toStrictEqual('https://example.com:9090/');
223+
});
224+
225+
it('should include anchor when present', () => {
226+
expect(stringify(parse('https://example.com/page#section'))).toStrictEqual('https://example.com/page#section');
227+
});
228+
229+
it('should default path to / when path is empty', () => {
230+
const parsed = parse('https://example.com');
231+
232+
parsed.path = '';
233+
234+
expect(stringify(parsed)).toStrictEqual('https://example.com/');
235+
});
236+
237+
it('should include query parameters', () => {
238+
expect(stringify(parse('https://example.com/path?a=1&b=2'))).toStrictEqual('https://example.com/path?a=1&b=2');
239+
});
240+
241+
it('should round-trip a complex URL', () => {
242+
const url = 'https://user:pass@example.com:8080/some/path?key=value&other=test#anchor';
243+
244+
expect(stringify(parse(url))).toStrictEqual(url);
245+
});
246+
});

0 commit comments

Comments
 (0)