Skip to content

Commit a010ca8

Browse files
committed
test: 新增 router 和 eventRoutes 的單元測試
1 parent 558792e commit a010ca8

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

test/_index.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
const mocks = vi.hoisted(() => ({
4+
router: vi.fn(),
5+
}));
6+
7+
vi.mock("../src/router", () => ({
8+
router: mocks.router,
9+
}));
10+
11+
import worker from "../src/_index";
12+
13+
describe("worker fetch entry", () => {
14+
it("delegates request handling to router", async () => {
15+
const request = new Request("https://example.com", { method: "POST" });
16+
const env = { SOME_ENV: "value" } as any;
17+
const ctx = { waitUntil: vi.fn() } as any;
18+
const expectedResponse = new Response("delegated", { status: 200 });
19+
20+
mocks.router.mockResolvedValueOnce(expectedResponse);
21+
22+
const response = await worker.fetch(request, env, ctx);
23+
24+
expect(mocks.router).toHaveBeenCalledWith(request, env, ctx);
25+
expect(response).toBe(expectedResponse);
26+
});
27+
});

test/router.extra.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { hmacSHA256Base64 } from "../src/utils/hmacSHA256Base64";
3+
4+
const mocks = vi.hoisted(() => ({
5+
eventRouter: vi.fn().mockResolvedValue(undefined),
6+
}));
7+
8+
vi.mock("../src/eventRouter", () => ({
9+
eventRouter: mocks.eventRouter,
10+
}));
11+
12+
import { router } from "../src/router";
13+
14+
const mockEnv = {
15+
LINE_CHANNEL_SECRET: { get: vi.fn() },
16+
LINE_CHANNEL_ACCESS_TOKEN: { get: vi.fn() },
17+
};
18+
19+
describe("router extra branches", () => {
20+
beforeEach(() => {
21+
vi.clearAllMocks();
22+
mockEnv.LINE_CHANNEL_SECRET.get.mockResolvedValue("secret");
23+
mockEnv.LINE_CHANNEL_ACCESS_TOKEN.get.mockResolvedValue("accessToken");
24+
});
25+
26+
it("returns 400 when body is not valid JSON", async () => {
27+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
28+
const request = new Request("https://example.com", {
29+
method: "POST",
30+
headers: { "x-line-signature": "sig" },
31+
body: "{",
32+
});
33+
34+
const response = await router(request, mockEnv as any, {} as any);
35+
36+
expect(response.status).toBe(400);
37+
expect(await response.text()).toBe("Invalid JSON");
38+
errorSpy.mockRestore();
39+
});
40+
41+
it("dispatches each event to eventRouter", async () => {
42+
const eventA = { type: "message", message: { type: "text", text: "Hi" } };
43+
const eventB = { type: "message", message: { type: "text", text: "hello" } };
44+
const rawBody = JSON.stringify({ destination: "", events: [eventA, eventB] });
45+
const signature = await hmacSHA256Base64("secret", rawBody);
46+
47+
const request = new Request("https://example.com", {
48+
method: "POST",
49+
headers: { "x-line-signature": signature },
50+
body: rawBody,
51+
});
52+
const ctx = { waitUntil: vi.fn() } as any;
53+
54+
const response = await router(request, mockEnv as any, ctx);
55+
56+
expect(response.status).toBe(200);
57+
expect(mocks.eventRouter).toHaveBeenCalledTimes(2);
58+
expect(mocks.eventRouter).toHaveBeenNthCalledWith(1, eventA, "accessToken", ctx);
59+
expect(mocks.eventRouter).toHaveBeenNthCalledWith(2, eventB, "accessToken", ctx);
60+
});
61+
62+
it("returns 200 when events field is missing", async () => {
63+
const rawBody = JSON.stringify({ destination: "" });
64+
const signature = await hmacSHA256Base64("secret", rawBody);
65+
66+
const request = new Request("https://example.com", {
67+
method: "POST",
68+
headers: { "x-line-signature": signature },
69+
body: rawBody,
70+
});
71+
const ctx = { waitUntil: vi.fn() } as any;
72+
73+
const response = await router(request, mockEnv as any, ctx);
74+
75+
expect(response.status).toBe(200);
76+
expect(mocks.eventRouter).not.toHaveBeenCalled();
77+
});
78+
});

test/utils/eventRoutes.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
2+
import { loadStart, markAsRead, sendMessage } from "../../src/utils/eventRoutes";
3+
4+
describe("eventRoutes", () => {
5+
const fetchMock = vi.fn();
6+
7+
beforeEach(() => {
8+
vi.clearAllMocks();
9+
vi.stubGlobal("fetch", fetchMock);
10+
});
11+
12+
afterAll(() => {
13+
vi.unstubAllGlobals();
14+
});
15+
16+
it("sendMessage calls LINE reply API with expected payload", async () => {
17+
fetchMock.mockResolvedValueOnce({
18+
json: vi.fn().mockResolvedValueOnce({ ok: true }),
19+
});
20+
21+
const result = await sendMessage("token-123", "reply-123", [{ type: "text", text: "Hello" }]);
22+
23+
expect(fetchMock).toHaveBeenCalledWith("https://api.line.me/v2/bot/message/reply", {
24+
headers: {
25+
"Content-Type": "application/json",
26+
Authorization: "Bearer token-123",
27+
},
28+
method: "POST",
29+
body: JSON.stringify({
30+
replyToken: "reply-123",
31+
messages: [{ type: "text", text: "Hello" }],
32+
}),
33+
});
34+
expect(result).toEqual({ ok: true });
35+
});
36+
37+
it("markAsRead calls LINE markAsRead API with expected payload", async () => {
38+
fetchMock.mockResolvedValueOnce({
39+
json: vi.fn().mockResolvedValueOnce({ ok: true }),
40+
});
41+
42+
const result = await markAsRead("token-123", "mark-123");
43+
44+
expect(fetchMock).toHaveBeenCalledWith("https://api.line.me/v2/bot/chat/markAsRead", {
45+
headers: {
46+
"Content-Type": "application/json",
47+
Authorization: "Bearer token-123",
48+
},
49+
method: "POST",
50+
body: JSON.stringify({
51+
markAsReadToken: "mark-123",
52+
}),
53+
});
54+
expect(result).toEqual({ ok: true });
55+
});
56+
57+
it("loadStart calls LINE loading API with expected payload", async () => {
58+
fetchMock.mockResolvedValueOnce({
59+
json: vi.fn().mockResolvedValueOnce({ ok: true }),
60+
});
61+
62+
const result = await loadStart("token-123", "U123", 5);
63+
64+
expect(fetchMock).toHaveBeenCalledWith("https://api.line.me/v2/bot/chat/loading/start", {
65+
headers: {
66+
"Content-Type": "application/json",
67+
Authorization: "Bearer token-123",
68+
},
69+
method: "POST",
70+
body: JSON.stringify({
71+
chatId: "U123",
72+
loadingSeconds: 5,
73+
}),
74+
});
75+
expect(result).toEqual({ ok: true });
76+
});
77+
78+
it("barrel index re-exports all event route helpers", async () => {
79+
const barrel = await import("../../src/utils/eventRoutes/index");
80+
81+
expect(typeof barrel.sendMessage).toBe("function");
82+
expect(typeof barrel.markAsRead).toBe("function");
83+
expect(typeof barrel.loadStart).toBe("function");
84+
});
85+
});

0 commit comments

Comments
 (0)