Skip to content

Commit 1e6d157

Browse files
authored
cover fetchEvent with a few unit tests (#1958)
1 parent 7282ef4 commit 1e6d157

File tree

5 files changed

+495
-257
lines changed

5 files changed

+495
-257
lines changed

.github/workflows/tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ jobs:
3737
- name: Run unit tests
3838
run: pnpm --filter tests unit:ci
3939

40+
- name: Run start unit tests
41+
run: pnpm --filter @solidjs/start test:ci
42+
4043
- name: E2E Chromium
4144
uses: cypress-io/github-action@v6
4245
with:

packages/start/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
},
1717
"scripts": {
1818
"typecheck": "tsc --noEmit",
19-
"build": "node build && pnpm tsc"
19+
"build": "node build && pnpm tsc",
20+
"test": "vitest",
21+
"test:ci": "vitest run"
2022
},
2123
"files": [
2224
"config",
@@ -60,7 +62,8 @@
6062
},
6163
"devDependencies": {
6264
"solid-js": "^1.9.5",
63-
"vinxi": "^0.5.7"
65+
"vinxi": "^0.5.7",
66+
"vitest": "3.0.5"
6467
},
6568
"dependencies": {
6669
"@tanstack/server-functions-plugin": "1.121.21",
Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
import { H3Event } from "vinxi/http";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import { cloneEvent, createFetchEvent, getFetchEvent, mergeResponseHeaders } from "./fetchEvent";
4+
5+
vi.mock("vinxi/http", () => ({
6+
getWebRequest: vi.fn(),
7+
getRequestIP: vi.fn(),
8+
getResponseStatus: vi.fn(),
9+
setResponseStatus: vi.fn(),
10+
getResponseStatusText: vi.fn(),
11+
getResponseHeader: vi.fn(),
12+
getResponseHeaders: vi.fn(),
13+
setResponseHeader: vi.fn(),
14+
appendResponseHeader: vi.fn(),
15+
removeResponseHeader: vi.fn()
16+
}));
17+
18+
import * as vinxiHttp from "vinxi/http";
19+
const mockedVinxiHttp = vi.mocked(vinxiHttp);
20+
21+
const createMockH3Event = (): H3Event => {
22+
const mockRequest = new Request("http://localhost/test");
23+
const mockStatus = 200;
24+
const mockStatusText = "OK";
25+
26+
return {
27+
node: {
28+
req: {},
29+
res: {
30+
statusCode: mockStatus,
31+
statusMessage: mockStatusText
32+
}
33+
},
34+
context: {},
35+
web: {
36+
request: mockRequest
37+
}
38+
} as H3Event;
39+
};
40+
41+
describe("fetchEvent", () => {
42+
let mockH3Event: H3Event;
43+
44+
beforeEach(() => {
45+
mockH3Event = createMockH3Event();
46+
vi.clearAllMocks();
47+
48+
mockedVinxiHttp.getWebRequest.mockReturnValue(mockH3Event.web?.request!);
49+
mockedVinxiHttp.getRequestIP.mockReturnValue("127.0.0.1");
50+
mockedVinxiHttp.getResponseStatus.mockReturnValue(200);
51+
mockedVinxiHttp.setResponseStatus.mockImplementation(() => {});
52+
mockedVinxiHttp.getResponseStatusText.mockReturnValue("OK");
53+
mockedVinxiHttp.getResponseHeader.mockReturnValue(undefined);
54+
mockedVinxiHttp.getResponseHeaders.mockReturnValue({});
55+
mockedVinxiHttp.setResponseHeader.mockImplementation(() => {});
56+
mockedVinxiHttp.appendResponseHeader.mockImplementation(() => {});
57+
mockedVinxiHttp.removeResponseHeader.mockImplementation(() => {});
58+
});
59+
60+
describe("createFetchEvent", () => {
61+
it("should create a FetchEvent from H3Event", () => {
62+
const fetchEvent = createFetchEvent(mockH3Event);
63+
64+
expect(fetchEvent).toEqual({
65+
request: mockH3Event.web?.request,
66+
response: expect.any(Object),
67+
clientAddress: "127.0.0.1",
68+
locals: {},
69+
nativeEvent: mockH3Event
70+
});
71+
});
72+
73+
it("should create response stub with correct properties", () => {
74+
const fetchEvent = createFetchEvent(mockH3Event);
75+
76+
expect(fetchEvent.response).toHaveProperty("status");
77+
expect(fetchEvent.response).toHaveProperty("statusText");
78+
expect(fetchEvent.response).toHaveProperty("headers");
79+
});
80+
});
81+
82+
describe("cloneEvent", () => {
83+
it("should create a shallow copy of FetchEvent", () => {
84+
const original = createFetchEvent(mockH3Event);
85+
const cloned = cloneEvent(original);
86+
87+
expect(cloned).toEqual(original);
88+
expect(cloned).not.toBe(original);
89+
expect(cloned.request).toBe(original.request);
90+
expect(cloned.response).toBe(original.response);
91+
});
92+
});
93+
94+
describe("getFetchEvent", () => {
95+
it("should create and cache FetchEvent on first call", () => {
96+
const fetchEvent = getFetchEvent(mockH3Event);
97+
98+
expect(mockH3Event.context.solidFetchEvent).toBe(fetchEvent);
99+
expect(fetchEvent.nativeEvent).toBe(mockH3Event);
100+
});
101+
102+
it("should return cached FetchEvent on subsequent calls", () => {
103+
const firstCall = getFetchEvent(mockH3Event);
104+
const secondCall = getFetchEvent(mockH3Event);
105+
106+
expect(firstCall).toBe(secondCall);
107+
});
108+
});
109+
110+
describe("mergeResponseHeaders", () => {
111+
it("should merge headers from Headers object to H3Event", () => {
112+
const headers = new Headers({
113+
"content-type": "application/json",
114+
"x-custom": "value"
115+
});
116+
117+
mergeResponseHeaders(mockH3Event, headers);
118+
119+
expect(mockedVinxiHttp.appendResponseHeader).toHaveBeenCalledWith(
120+
mockH3Event,
121+
"content-type",
122+
"application/json"
123+
);
124+
expect(mockedVinxiHttp.appendResponseHeader).toHaveBeenCalledWith(
125+
mockH3Event,
126+
"x-custom",
127+
"value"
128+
);
129+
});
130+
});
131+
132+
describe("ResponseStub", () => {
133+
let fetchEvent: any;
134+
135+
beforeEach(() => {
136+
fetchEvent = createFetchEvent(mockH3Event);
137+
});
138+
139+
describe("status", () => {
140+
it("should get status from H3Event", () => {
141+
expect(fetchEvent.response.status).toBe(200);
142+
});
143+
144+
it("should set status on H3Event", () => {
145+
fetchEvent.response.status = 404;
146+
expect(mockedVinxiHttp.setResponseStatus).toHaveBeenCalledWith(mockH3Event, 404);
147+
});
148+
});
149+
150+
describe("statusText", () => {
151+
it("should get statusText from H3Event", () => {
152+
expect(fetchEvent.response.statusText).toBe("OK");
153+
});
154+
155+
it("should set statusText on H3Event", () => {
156+
fetchEvent.response.statusText = "Not Found";
157+
expect(mockedVinxiHttp.setResponseStatus).toHaveBeenCalledWith(
158+
mockH3Event,
159+
200,
160+
"Not Found"
161+
);
162+
});
163+
});
164+
});
165+
166+
describe("HeaderProxy", () => {
167+
let fetchEvent: any;
168+
169+
beforeEach(() => {
170+
fetchEvent = createFetchEvent(mockH3Event);
171+
});
172+
173+
describe("get", () => {
174+
it("should return null for non-existent header", () => {
175+
expect(fetchEvent.response.headers.get("non-existent")).toBe(null);
176+
});
177+
178+
it("should return string value for single header", () => {
179+
mockedVinxiHttp.getResponseHeader.mockReturnValue("application/json");
180+
expect(fetchEvent.response.headers.get("content-type")).toBe("application/json");
181+
});
182+
183+
it("should join array values with comma", () => {
184+
mockedVinxiHttp.getResponseHeader.mockReturnValue(["text/html", "application/json"]);
185+
expect(fetchEvent.response.headers.get("accept")).toBe("text/html, application/json");
186+
});
187+
});
188+
189+
describe("has", () => {
190+
it("should return false for non-existent header", () => {
191+
mockedVinxiHttp.getResponseHeader.mockReturnValue(undefined);
192+
expect(fetchEvent.response.headers.has("non-existent")).toBe(false);
193+
});
194+
195+
it("should return true for existing header", () => {
196+
mockedVinxiHttp.getResponseHeader.mockReturnValue("application/json");
197+
expect(fetchEvent.response.headers.has("content-type")).toBe(true);
198+
});
199+
});
200+
201+
describe("set", () => {
202+
it("should set header value", () => {
203+
fetchEvent.response.headers.set("content-type", "application/json");
204+
expect(mockedVinxiHttp.setResponseHeader).toHaveBeenCalledWith(
205+
mockH3Event,
206+
"content-type",
207+
"application/json"
208+
);
209+
});
210+
});
211+
212+
describe("delete", () => {
213+
it("should remove header", () => {
214+
fetchEvent.response.headers.delete("content-type");
215+
expect(mockedVinxiHttp.removeResponseHeader).toHaveBeenCalledWith(
216+
mockH3Event,
217+
"content-type"
218+
);
219+
});
220+
});
221+
222+
describe("append", () => {
223+
it("should append header value", () => {
224+
fetchEvent.response.headers.append("x-custom", "value");
225+
expect(mockedVinxiHttp.appendResponseHeader).toHaveBeenCalledWith(
226+
mockH3Event,
227+
"x-custom",
228+
"value"
229+
);
230+
});
231+
});
232+
233+
describe("getSetCookie", () => {
234+
it("should return array for single cookie", () => {
235+
mockedVinxiHttp.getResponseHeader.mockReturnValue("session=abc123");
236+
expect(fetchEvent.response.headers.getSetCookie()).toEqual(["session=abc123"]);
237+
});
238+
239+
it("should return array for multiple cookies", () => {
240+
mockedVinxiHttp.getResponseHeader.mockReturnValue(["session=abc123", "theme=dark"]);
241+
expect(fetchEvent.response.headers.getSetCookie()).toEqual([
242+
"session=abc123",
243+
"theme=dark"
244+
]);
245+
});
246+
});
247+
248+
describe("forEach", () => {
249+
it("should iterate over headers", () => {
250+
mockedVinxiHttp.getResponseHeaders.mockReturnValue({
251+
"content-type": "application/json",
252+
"x-custom": ["value1", "value2"]
253+
});
254+
255+
const callback = vi.fn();
256+
fetchEvent.response.headers.forEach(callback);
257+
258+
expect(callback).toHaveBeenCalledWith(
259+
"application/json",
260+
"content-type",
261+
expect.any(Object)
262+
);
263+
expect(callback).toHaveBeenCalledWith("value1, value2", "x-custom", expect.any(Object));
264+
});
265+
});
266+
267+
describe("entries", () => {
268+
it("should return iterator of header entries", () => {
269+
mockedVinxiHttp.getResponseHeaders.mockReturnValue({
270+
"content-type": "application/json",
271+
"x-custom": ["value1", "value2"]
272+
});
273+
274+
const entries = Array.from(fetchEvent.response.headers.entries());
275+
expect(entries).toEqual([
276+
["content-type", "application/json"],
277+
["x-custom", "value1, value2"]
278+
]);
279+
});
280+
});
281+
282+
describe("keys", () => {
283+
it("should return iterator of header keys", () => {
284+
mockedVinxiHttp.getResponseHeaders.mockReturnValue({
285+
"content-type": "application/json",
286+
"x-custom": "value"
287+
});
288+
289+
const keys = Array.from(fetchEvent.response.headers.keys());
290+
expect(keys).toEqual(["content-type", "x-custom"]);
291+
});
292+
});
293+
294+
describe("values", () => {
295+
it("should return iterator of header values", () => {
296+
mockedVinxiHttp.getResponseHeaders.mockReturnValue({
297+
"content-type": "application/json",
298+
"x-custom": ["value1", "value2"]
299+
});
300+
301+
const values = Array.from(fetchEvent.response.headers.values());
302+
expect(values).toEqual(["application/json", "value1, value2"]);
303+
});
304+
});
305+
306+
describe("Symbol.iterator", () => {
307+
it("should be iterable", () => {
308+
mockedVinxiHttp.getResponseHeaders.mockReturnValue({
309+
"content-type": "application/json",
310+
"x-custom": "value"
311+
});
312+
313+
const entries = Array.from(fetchEvent.response.headers);
314+
expect(entries).toEqual([
315+
["content-type", "application/json"],
316+
["x-custom", "value"]
317+
]);
318+
});
319+
});
320+
});
321+
});

packages/start/vitest.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
globals: true,
6+
environment: "node",
7+
mockReset: true
8+
}
9+
});

0 commit comments

Comments
 (0)