Skip to content

Commit 3e1ae70

Browse files
Varixowmertens
authored andcommitted
test: add path handler unit tests
1 parent b202c40 commit 3e1ae70

File tree

2 files changed

+291
-24
lines changed

2 files changed

+291
-24
lines changed

packages/qwik-router/src/middleware/request-handler/handlers/loader-handler.unit.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,13 @@ vi.mock('./validator-utils', () => ({
5454
}));
5555

5656
function createMockLoader(id: string, hash: string, result: unknown): Mocked<LoaderInternal> {
57-
const mockLoaderFunction = (): Mocked<LoaderSignal<unknown>> => ({
58-
value: Promise.resolve(result),
59-
});
57+
const mockLoaderFunction = (): Mocked<LoaderSignal<unknown>> =>
58+
({
59+
value: Promise.resolve(result),
60+
force: vi.fn(),
61+
invalidate: vi.fn(),
62+
refetch: vi.fn(),
63+
}) as unknown as Mocked<LoaderSignal<unknown>>;
6064

6165
return {
6266
__brand: 'server_loader' as const,
@@ -379,20 +383,13 @@ describe('loaderHandler', () => {
379383
describe('loadersMiddleware', () => {
380384
let mockRequestEvent: Mocked<RequestEventInternal>;
381385
let mockLoader: Mocked<LoaderInternal>;
382-
let mockQwikSerializer: Mocked<QwikSerializer>;
383386
let mockLoaders: Record<string, any>;
384387

385388
beforeEach(() => {
386389
vi.clearAllMocks();
387390

388391
mockLoader = createMockLoader('test-loader-id', 'test-hash', { result: 'success' });
389392

390-
mockQwikSerializer = {
391-
_serialize: vi.fn(),
392-
_deserialize: vi.fn(),
393-
_verifySerializable: vi.fn(),
394-
} as Mocked<QwikSerializer>;
395-
396393
mockLoaders = {};
397394

398395
mockRequestEvent = {
@@ -487,19 +484,12 @@ describe('loadersMiddleware', () => {
487484
describe('loaderDataHandler', () => {
488485
let mockRequestEvent: Mocked<RequestEventInternal>;
489486
let mockLoader: Mocked<LoaderInternal>;
490-
let mockQwikSerializer: Mocked<QwikSerializer>;
491487

492488
beforeEach(() => {
493489
vi.clearAllMocks();
494490

495491
mockLoader = createMockLoader('test-loader-id', 'test-hash', { result: 'success' });
496492

497-
mockQwikSerializer = {
498-
_serialize: vi.fn(),
499-
_deserialize: vi.fn(),
500-
_verifySerializable: vi.fn(),
501-
} as Mocked<QwikSerializer>;
502-
503493
mockRequestEvent = {
504494
sharedMap: new Map(),
505495
headersSent: false,
@@ -657,7 +647,6 @@ describe('loaderDataHandler', () => {
657647
describe('executeLoader', () => {
658648
let mockRequestEvent: Mocked<RequestEventInternal>;
659649
let mockLoader: Mocked<LoaderInternal>;
660-
let mockQwikSerializer: Mocked<QwikSerializer>;
661650
let mockLoaders: Record<string, any>;
662651
let mockSerializationStrategyMap: Map<string, any>;
663652

@@ -666,12 +655,6 @@ describe('executeLoader', () => {
666655

667656
mockLoader = createMockLoader('test-loader-id', 'test-hash', { result: 'success' });
668657

669-
mockQwikSerializer = {
670-
_serialize: vi.fn(),
671-
_deserialize: vi.fn(),
672-
_verifySerializable: vi.fn(),
673-
} as Mocked<QwikSerializer>;
674-
675658
mockLoaders = {};
676659
mockSerializationStrategyMap = new Map();
677660

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
2+
import { fixTrailingSlash } from './path-handler';
3+
import type { RequestEvent } from '../types';
4+
import { isQDataRequestBasedOnSharedMap } from '../resolve-request-handlers';
5+
import { HttpStatus } from '../http-status-codes';
6+
7+
// Mock dependencies
8+
vi.mock('../request-event', () => ({
9+
getRequestTrailingSlash: vi.fn(),
10+
}));
11+
12+
vi.mock('../resolve-request-handlers', () => ({
13+
isQDataRequestBasedOnSharedMap: vi.fn(),
14+
}));
15+
16+
describe('fixTrailingSlash', () => {
17+
let mockRequestEvent: Mocked<RequestEvent>;
18+
19+
beforeEach(() => {
20+
// Reset all mocks
21+
vi.clearAllMocks();
22+
23+
// Create mock request event
24+
mockRequestEvent = {
25+
basePathname: '/',
26+
originalUrl: new URL('http://localhost/test'),
27+
sharedMap: new Map(),
28+
redirect: vi.fn(),
29+
} as unknown as Mocked<RequestEvent>;
30+
31+
// Set up default mocks
32+
globalThis.__NO_TRAILING_SLASH__ = true;
33+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(false);
34+
});
35+
36+
describe('when it is a QData request', () => {
37+
it('should not check for trailing slash redirects', () => {
38+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(true);
39+
mockRequestEvent.originalUrl.pathname = '/test';
40+
mockRequestEvent.originalUrl.search = '?param=value';
41+
42+
expect(() => fixTrailingSlash(mockRequestEvent)).not.toThrow();
43+
expect(mockRequestEvent.redirect).not.toHaveBeenCalled();
44+
});
45+
});
46+
47+
describe('when pathname ends with .html', () => {
48+
it('should not check for trailing slash redirects', () => {
49+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(false);
50+
mockRequestEvent.originalUrl.pathname = '/test.html';
51+
mockRequestEvent.originalUrl.search = '?param=value';
52+
53+
expect(() => fixTrailingSlash(mockRequestEvent)).not.toThrow();
54+
expect(mockRequestEvent.redirect).not.toHaveBeenCalled();
55+
});
56+
});
57+
58+
describe('when pathname equals basePathname', () => {
59+
it('should not check for trailing slash redirects', () => {
60+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(false);
61+
const mockEvent = {
62+
...mockRequestEvent,
63+
basePathname: '/',
64+
originalUrl: new URL('http://localhost/'),
65+
};
66+
mockEvent.originalUrl.pathname = '/';
67+
mockEvent.originalUrl.search = '?param=value';
68+
69+
expect(() => fixTrailingSlash(mockEvent)).not.toThrow();
70+
expect(mockEvent.redirect).not.toHaveBeenCalled();
71+
});
72+
});
73+
74+
describe('when trailing slash is required (trailingSlash = true)', () => {
75+
beforeEach(() => {
76+
globalThis.__NO_TRAILING_SLASH__ = false;
77+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(false);
78+
});
79+
80+
it('should redirect when pathname does not end with slash', () => {
81+
mockRequestEvent.originalUrl.pathname = '/test';
82+
mockRequestEvent.originalUrl.search = '?param=value';
83+
84+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
85+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
86+
HttpStatus.MovedPermanently,
87+
'/test/?param=value'
88+
);
89+
});
90+
91+
it('should not redirect when pathname already ends with slash', () => {
92+
mockRequestEvent.originalUrl.pathname = '/test/';
93+
mockRequestEvent.originalUrl.search = '?param=value';
94+
95+
expect(() => fixTrailingSlash(mockRequestEvent)).not.toThrow();
96+
expect(mockRequestEvent.redirect).not.toHaveBeenCalled();
97+
});
98+
99+
it('should handle pathname with multiple segments', () => {
100+
mockRequestEvent.originalUrl.pathname = '/test/path';
101+
mockRequestEvent.originalUrl.search = '?param=value';
102+
103+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
104+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
105+
HttpStatus.MovedPermanently,
106+
'/test/path/?param=value'
107+
);
108+
});
109+
110+
it('should handle pathname with no search params', () => {
111+
mockRequestEvent.originalUrl.pathname = '/test';
112+
mockRequestEvent.originalUrl.search = '';
113+
114+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
115+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(HttpStatus.MovedPermanently, '/test/');
116+
});
117+
118+
it('should handle pathname with complex search params', () => {
119+
mockRequestEvent.originalUrl.pathname = '/test/path';
120+
mockRequestEvent.originalUrl.search = '?param1=value1&param2=value2&param3=value3';
121+
122+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
123+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
124+
HttpStatus.MovedPermanently,
125+
'/test/path/?param1=value1&param2=value2&param3=value3'
126+
);
127+
});
128+
});
129+
130+
describe('when trailing slash is not required (trailingSlash = false)', () => {
131+
beforeEach(() => {
132+
globalThis.__NO_TRAILING_SLASH__ = true;
133+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(false);
134+
});
135+
136+
it('should redirect when pathname ends with slash', () => {
137+
mockRequestEvent.originalUrl.pathname = '/test/';
138+
mockRequestEvent.originalUrl.search = '?param=value';
139+
140+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
141+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
142+
HttpStatus.MovedPermanently,
143+
'/test?param=value'
144+
);
145+
});
146+
147+
it('should not redirect when pathname does not end with slash', () => {
148+
mockRequestEvent.originalUrl.pathname = '/test';
149+
mockRequestEvent.originalUrl.search = '?param=value';
150+
151+
expect(() => fixTrailingSlash(mockRequestEvent)).not.toThrow();
152+
expect(mockRequestEvent.redirect).not.toHaveBeenCalled();
153+
});
154+
155+
it('should handle pathname with multiple segments ending with slash', () => {
156+
mockRequestEvent.originalUrl.pathname = '/test/path/';
157+
mockRequestEvent.originalUrl.search = '?param=value';
158+
159+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
160+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
161+
HttpStatus.MovedPermanently,
162+
'/test/path?param=value'
163+
);
164+
});
165+
166+
it('should handle pathname with no search params', () => {
167+
mockRequestEvent.originalUrl.pathname = '/test/';
168+
mockRequestEvent.originalUrl.search = '';
169+
170+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
171+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(HttpStatus.MovedPermanently, '/test');
172+
});
173+
174+
it('should handle pathname with complex search params', () => {
175+
mockRequestEvent.originalUrl.pathname = '/test/path/';
176+
mockRequestEvent.originalUrl.search = '?param1=value1&param2=value2&param3=value3';
177+
178+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
179+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
180+
HttpStatus.MovedPermanently,
181+
'/test/path?param1=value1&param2=value2&param3=value3'
182+
);
183+
});
184+
});
185+
186+
describe('edge cases', () => {
187+
beforeEach(() => {
188+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(false);
189+
});
190+
191+
it('should handle pathname with only slash', () => {
192+
globalThis.__NO_TRAILING_SLASH__ = true;
193+
mockRequestEvent.originalUrl.pathname = '/';
194+
mockRequestEvent.originalUrl.search = '';
195+
196+
expect(() => fixTrailingSlash(mockRequestEvent)).not.toThrow();
197+
expect(mockRequestEvent.redirect).not.toHaveBeenCalled();
198+
});
199+
200+
it('should handle pathname with multiple trailing slashes', () => {
201+
globalThis.__NO_TRAILING_SLASH__ = true;
202+
mockRequestEvent.originalUrl.pathname = '/test///';
203+
mockRequestEvent.originalUrl.search = '?param=value';
204+
205+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
206+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
207+
HttpStatus.MovedPermanently,
208+
'/test//?param=value'
209+
);
210+
});
211+
212+
it('should handle pathname with special characters', () => {
213+
globalThis.__NO_TRAILING_SLASH__ = false;
214+
mockRequestEvent.originalUrl.pathname = '/test-path_with_underscores';
215+
mockRequestEvent.originalUrl.search = '?param=value';
216+
217+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
218+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
219+
HttpStatus.MovedPermanently,
220+
'/test-path_with_underscores/?param=value'
221+
);
222+
});
223+
224+
it('should handle pathname with numbers', () => {
225+
globalThis.__NO_TRAILING_SLASH__ = false;
226+
mockRequestEvent.originalUrl.pathname = '/test123';
227+
mockRequestEvent.originalUrl.search = '?param=value';
228+
229+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
230+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
231+
HttpStatus.MovedPermanently,
232+
'/test123/?param=value'
233+
);
234+
});
235+
236+
it('should handle pathname with encoded characters', () => {
237+
globalThis.__NO_TRAILING_SLASH__ = false;
238+
mockRequestEvent.originalUrl.pathname = '/test%20path';
239+
mockRequestEvent.originalUrl.search = '?param=value';
240+
241+
expect(() => fixTrailingSlash(mockRequestEvent)).toThrow();
242+
expect(mockRequestEvent.redirect).toHaveBeenCalledWith(
243+
HttpStatus.MovedPermanently,
244+
'/test%20path/?param=value'
245+
);
246+
});
247+
});
248+
249+
describe('integration with other handlers', () => {
250+
it('should work correctly with QData requests', () => {
251+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(true);
252+
globalThis.__NO_TRAILING_SLASH__ = false;
253+
mockRequestEvent.originalUrl.pathname = '/test';
254+
mockRequestEvent.originalUrl.search = '?param=value';
255+
256+
expect(() => fixTrailingSlash(mockRequestEvent)).not.toThrow();
257+
expect(mockRequestEvent.redirect).not.toHaveBeenCalled();
258+
});
259+
260+
it('should work correctly with HTML files', () => {
261+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(false);
262+
globalThis.__NO_TRAILING_SLASH__ = false;
263+
mockRequestEvent.originalUrl.pathname = '/test.html';
264+
mockRequestEvent.originalUrl.search = '?param=value';
265+
266+
expect(() => fixTrailingSlash(mockRequestEvent)).not.toThrow();
267+
expect(mockRequestEvent.redirect).not.toHaveBeenCalled();
268+
});
269+
270+
it('should work correctly with base pathname', () => {
271+
vi.mocked(isQDataRequestBasedOnSharedMap).mockReturnValue(false);
272+
globalThis.__NO_TRAILING_SLASH__ = false;
273+
const mockEvent = {
274+
...mockRequestEvent,
275+
basePathname: '/app',
276+
originalUrl: new URL('http://localhost/app'),
277+
};
278+
mockEvent.originalUrl.search = '?param=value';
279+
280+
expect(() => fixTrailingSlash(mockEvent)).not.toThrow();
281+
expect(mockEvent.redirect).not.toHaveBeenCalled();
282+
});
283+
});
284+
});

0 commit comments

Comments
 (0)