Skip to content

Commit e69bbd6

Browse files
fix: preserve trailing optional path parameters (#2169)
Co-authored-by: Kai Spencer <51139521+KaiSpencer@users.noreply.github.com>
1 parent 540e0ac commit e69bbd6

9 files changed

Lines changed: 94 additions & 20 deletions

File tree

src/core/utils/matching/matchRequestUrl.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ describe('matchRequestUrl', () => {
6161
expect(match).toHaveProperty('matches', false)
6262
expect(match).toHaveProperty('params', {})
6363
})
64+
65+
test('returns true when matching optional path parameters', () => {
66+
const match = matchRequestUrl(
67+
new URL('https://test.mswjs.io/user'),
68+
'https://test.mswjs.io/user/:userId?',
69+
)
70+
expect(match).toHaveProperty('matches', true)
71+
expect(match).toHaveProperty('params', {
72+
userId: undefined,
73+
})
74+
})
6475
})
6576

6677
describe('coercePath', () => {

src/core/utils/matching/normalizePath.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,14 @@ test('returns a path pattern string as-is', () => {
4343
expect(normalizePath('*/resource/*')).toEqual('*/resource/*')
4444
})
4545

46-
test('removeß query parameters and hashes from a path pattern string', () => {
46+
test('removes query parameters and hashes from a path pattern string', () => {
4747
expect(normalizePath(':api/user?query=123#some')).toEqual(
4848
'http://localhost/:api/user',
4949
)
5050
})
51+
52+
test('preserves optional path parameters', () => {
53+
expect(normalizePath('/user/:userId?')).toEqual(
54+
'http://localhost/user/:userId?',
55+
)
56+
})

src/core/utils/matching/normalizePath.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getAbsoluteUrl } from '../url/getAbsoluteUrl'
88
* - Removes query parameters and hashes.
99
* - Rebases relative URLs against the "baseUrl" or the current location.
1010
* - Preserves relative URLs in Node.js, unless specified otherwise.
11+
* - Preserves optional path parameters.
1112
*/
1213
export function normalizePath(path: Path, baseUrl?: string): Path {
1314
// RegExp paths do not need normalization.
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import { cleanUrl } from './cleanUrl'
22

3-
test('removes query parameters from a URL string', () => {
3+
it('removes query parameters from a URL string', () => {
44
expect(cleanUrl('/user?id=123')).toEqual('/user')
55
expect(cleanUrl('/user?id=123&id=456')).toEqual('/user')
66
expect(cleanUrl('/user?id=123&role=admin')).toEqual('/user')
77
})
88

9-
test('removes hashes from a URL string', () => {
9+
it('removes hashes from a URL string', () => {
1010
expect(cleanUrl('/user#hash')).toEqual('/user')
1111
expect(cleanUrl('/user#hash-with-dashes')).toEqual('/user')
1212
})
1313

14-
test('removes both query parameters and hashes from a URL string', () => {
14+
it('removes both query parameters and hashes from a URL string', () => {
1515
expect(cleanUrl('/user?id=123#some')).toEqual('/user')
1616
expect(cleanUrl('/user?id=123&role=admin#some')).toEqual('/user')
1717
})
18+
19+
it('preserves optional path parameters', () => {
20+
expect(cleanUrl('/user/:id?')).toEqual('/user/:id?')
21+
expect(cleanUrl('/user/:id?/:messageId?')).toEqual('/user/:id?/:messageId?')
22+
})

src/core/utils/url/cleanUrl.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ export function getSearchParams(path: string) {
55
}
66

77
/**
8-
* Removes query parameters and hashes from a given URL string.
8+
* Removes search parameters and the fragment
9+
* from a given URL string.
910
*/
1011
export function cleanUrl(path: string): string {
12+
// If the path ends with an optional path parameter,
13+
// return it as-is.
14+
if (path.endsWith('?')) {
15+
return path
16+
}
17+
18+
// Otherwise, remove the search and fragment from it.
1119
return path.replace(REDUNDANT_CHARACTERS_EXP, '')
1220
}

src/core/utils/url/getAbsoluteUrl.node.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33
*/
44
import { getAbsoluteUrl } from './getAbsoluteUrl'
55

6-
test('returns a given relative URL as-is', () => {
6+
it('returns a given relative URL as-is', () => {
77
expect(getAbsoluteUrl('/reviews')).toBe('/reviews')
88
})
99

10-
test('rebases a relative URL against a custom base URL', () => {
10+
it('rebases a relative URL against a custom base URL', () => {
1111
expect(getAbsoluteUrl('/user', 'https://api.github.com')).toEqual(
1212
'https://api.github.com/user',
1313
)
1414
})
15-
test('returns a given absolute URL as-is', () => {
15+
it('returns a given absolute URL as-is', () => {
1616
expect(getAbsoluteUrl('https://api.mswjs.io/users')).toBe(
1717
'https://api.mswjs.io/users',
1818
)

src/core/utils/url/getAbsoluteUrl.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,27 @@
33
*/
44
import { getAbsoluteUrl } from './getAbsoluteUrl'
55

6-
test('rebases a relative URL against the current "baseURI" (default)', () => {
6+
it('rebases a relative URL against the current "baseURI" (default)', () => {
77
expect(getAbsoluteUrl('/reviews')).toEqual('http://localhost/reviews')
88
})
99

10-
test('rebases a relative URL against a custom base URL', () => {
10+
it('rebases a relative URL against a custom base URL', () => {
1111
expect(getAbsoluteUrl('/user', 'https://api.github.com')).toEqual(
1212
'https://api.github.com/user',
1313
)
1414
})
1515

16-
test('returns a given absolute URL as-is', () => {
16+
it('returns a given absolute URL as-is', () => {
1717
expect(getAbsoluteUrl('https://api.mswjs.io/users')).toEqual(
1818
'https://api.mswjs.io/users',
1919
)
2020
})
2121

22-
test('returns an absolute URL given a relative path without a leading slash', () => {
22+
it('returns an absolute URL given a relative path without a leading slash', () => {
2323
expect(getAbsoluteUrl('users')).toEqual('http://localhost/users')
2424
})
2525

26-
test('returns a path with a pattern as-is', () => {
26+
it('returns a path with a pattern as-is', () => {
2727
expect(getAbsoluteUrl(':api/user')).toEqual('http://localhost/:api/user')
2828
expect(getAbsoluteUrl('*/resource/*')).toEqual('*/resource/*')
2929
})

src/core/utils/url/isAbsoluteUrl.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,30 @@
33
*/
44
import { isAbsoluteUrl } from './isAbsoluteUrl'
55

6-
test('returns true for the "http" scheme', () => {
6+
it('returns true for the "http" scheme', () => {
77
expect(isAbsoluteUrl('http://www.domain.com')).toEqual(true)
88
})
99

10-
test('returns true for the "https" scheme', () => {
10+
it('returns true for the "https" scheme', () => {
1111
expect(isAbsoluteUrl('https://www.domain.com')).toEqual(true)
1212
})
1313

14-
test('returns true for the "ws" scheme', () => {
14+
it('returns true for the "ws" scheme', () => {
1515
expect(isAbsoluteUrl('ws://www.domain.com')).toEqual(true)
1616
})
1717

18-
test('returns true for the "ftp" scheme', () => {
18+
it('returns true for the "ftp" scheme', () => {
1919
expect(isAbsoluteUrl('ftp://www.domain.com')).toEqual(true)
2020
})
2121

22-
test('returns true for the custom scheme', () => {
22+
it('returns true for the custom scheme', () => {
2323
expect(isAbsoluteUrl('web+my://www.example.com')).toEqual(true)
2424
})
2525

26-
test('returns false for the relative URL', () => {
26+
it('returns false for the relative URL', () => {
2727
expect(isAbsoluteUrl('/test')).toEqual(false)
2828
})
2929

30-
test('returns false for the relative URL without a leading slash', () => {
30+
it('returns false for the relative URL without a leading slash', () => {
3131
expect(isAbsoluteUrl('test')).toEqual(false)
3232
})
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { HttpResponse, http } from 'msw'
5+
import { setupServer } from 'msw/node'
6+
7+
const server = setupServer()
8+
9+
beforeAll(() => {
10+
server.listen()
11+
})
12+
13+
afterEach(() => {
14+
server.resetHandlers()
15+
})
16+
17+
afterAll(() => {
18+
server.close()
19+
})
20+
21+
it('intercepts the request that fully matches the path', async () => {
22+
server.use(
23+
http.get('http://localhost/user/:id?', () =>
24+
HttpResponse.json({ mocked: true }),
25+
),
26+
)
27+
28+
const response = await fetch('http://localhost/user/123')
29+
expect(response.status).toBe(200)
30+
expect(await response.json()).toEqual({ mocked: true })
31+
})
32+
33+
it('intercepts the request that partially matches the path', async () => {
34+
server.use(
35+
http.get('http://localhost/user/:id?', () =>
36+
HttpResponse.json({ mocked: true }),
37+
),
38+
)
39+
40+
const response = await fetch('http://localhost/user')
41+
expect(response.status).toBe(200)
42+
expect(await response.json()).toEqual({ mocked: true })
43+
})

0 commit comments

Comments
 (0)