Add integration tests for batch review route behavior
swadhinbiswas committed 8 months ago
54e9ce2
1 parent 742a0c1
commit 54e9ce2
1 file changed
+190 -0
Showing 1 changed file+190-0
▼tests/integration/review-batch-route.test.ts
+190-0
| @@ -0,0 +1,190 @@ | ||
1 | +import { beforeEach, describe, expect, it, vi } from "vitest"; | |
2 | + | |
3 | +const { canReadRepoMock, canWriteRepoMock, triggerAutomationMock, fakeSchema } = vi.hoisted(() => ({ | |
4 | + canReadRepoMock: vi.fn(async () => true), | |
5 | + canWriteRepoMock: vi.fn(async () => true), | |
6 | + triggerAutomationMock: vi.fn(async () => undefined), | |
7 | + fakeSchema: { | |
8 | + users: { username: {} }, | |
9 | + repositories: { ownerId: {}, name: {}, id: {} }, | |
10 | + pullRequests: { repositoryId: {}, number: {}, id: {} }, | |
11 | + pullRequestReviews: { id: {} }, | |
12 | + pullRequestComments: { id: {} }, | |
13 | + } as any, | |
14 | +})); | |
15 | + | |
16 | +let mockDb: any; | |
17 | + | |
18 | +vi.mock("@/db", () => ({ | |
19 | + getDatabase: () => mockDb, | |
20 | + schema: fakeSchema, | |
21 | +})); | |
22 | + | |
23 | +vi.mock("@/lib/permissions", () => ({ | |
24 | + canReadRepo: canReadRepoMock, | |
25 | + canWriteRepo: canWriteRepoMock, | |
26 | +})); | |
27 | + | |
28 | +vi.mock("@/lib/automations", () => ({ | |
29 | + triggerAutomation: triggerAutomationMock, | |
30 | +})); | |
31 | + | |
32 | +import { POST as batchReviewPost } from "@/pages/api/repos/[owner]/[repo]/pulls/[number]/reviews/batch"; | |
33 | + | |
34 | +function makeDb(options?: { failCommentInsert?: boolean }) { | |
35 | + const owner = { id: "owner-1", username: "acme" }; | |
36 | + const repo = { id: "repo-1", ownerId: "owner-1", name: "demo" }; | |
37 | + const pr = { id: "pr-1", repositoryId: "repo-1", number: 42, authorId: "author-1" }; | |
38 | + | |
39 | + const insertCalls: Array<{ table: unknown; value: unknown }> = []; | |
40 | + const deleteTables: unknown[] = []; | |
41 | + | |
42 | + let commentInsertCount = 0; | |
43 | + | |
44 | + const db = { | |
45 | + query: { | |
46 | + users: { | |
47 | + findFirst: vi.fn(async () => owner), | |
48 | + }, | |
49 | + repositories: { | |
50 | + findFirst: vi.fn(async () => repo), | |
51 | + }, | |
52 | + pullRequests: { | |
53 | + findFirst: vi.fn(async () => pr), | |
54 | + }, | |
55 | + }, | |
56 | + insert: vi.fn((table: unknown) => ({ | |
57 | + values: vi.fn(async (value: unknown) => { | |
58 | + insertCalls.push({ table, value }); | |
59 | + | |
60 | + if (table === fakeSchema.pullRequestComments) { | |
61 | + commentInsertCount += 1; | |
62 | + if (options?.failCommentInsert && commentInsertCount === 2) { | |
63 | + throw new Error("comment insert failed"); | |
64 | + } | |
65 | + } | |
66 | + }), | |
67 | + })), | |
68 | + delete: vi.fn((table: unknown) => ({ | |
69 | + where: vi.fn(async () => { | |
70 | + deleteTables.push(table); | |
71 | + }), | |
72 | + })), | |
73 | + __state: { | |
74 | + insertCalls, | |
75 | + deleteTables, | |
76 | + }, | |
77 | + }; | |
78 | + | |
79 | + return db; | |
80 | +} | |
81 | + | |
82 | +async function readJson(response: Response): Promise<any> { | |
83 | + return response.json(); | |
84 | +} | |
85 | + | |
86 | +describe("batch review route", () => { | |
87 | + beforeEach(() => { | |
88 | + canReadRepoMock.mockResolvedValue(true); | |
89 | + canWriteRepoMock.mockResolvedValue(true); | |
90 | + triggerAutomationMock.mockClear(); | |
91 | + }); | |
92 | + | |
93 | + it("returns 401 when user is not authenticated", async () => { | |
94 | + mockDb = makeDb(); | |
95 | + | |
96 | + const response = await batchReviewPost({ | |
97 | + params: { owner: "acme", repo: "demo", number: "42" }, | |
98 | + request: new Request("http://localhost/api/repos/acme/demo/pulls/42/reviews/batch", { | |
99 | + method: "POST", | |
100 | + headers: { "content-type": "application/json" }, | |
101 | + body: JSON.stringify({ | |
102 | + state: "COMMENTED", | |
103 | + comments: [{ body: "looks good" }], | |
104 | + }), | |
105 | + }), | |
106 | + locals: {}, | |
107 | + } as any); | |
108 | + | |
109 | + expect(response.status).toBe(401); | |
110 | + }); | |
111 | + | |
112 | + it("returns 403 for APPROVED when user lacks write access", async () => { | |
113 | + mockDb = makeDb(); | |
114 | + canWriteRepoMock.mockResolvedValue(false); | |
115 | + | |
116 | + const response = await batchReviewPost({ | |
117 | + params: { owner: "acme", repo: "demo", number: "42" }, | |
118 | + request: new Request("http://localhost/api/repos/acme/demo/pulls/42/reviews/batch", { | |
119 | + method: "POST", | |
120 | + headers: { "content-type": "application/json" }, | |
121 | + body: JSON.stringify({ | |
122 | + state: "APPROVED", | |
123 | + comments: [{ body: "approved" }], | |
124 | + }), | |
125 | + }), | |
126 | + locals: { user: { id: "reviewer-1" } }, | |
127 | + } as any); | |
128 | + | |
129 | + const body = await readJson(response); | |
130 | + expect(response.status).toBe(403); | |
131 | + expect(body?.error?.code).toBe("FORBIDDEN"); | |
132 | + }); | |
133 | + | |
134 | + it("submits review and comments in one request", async () => { | |
135 | + mockDb = makeDb(); | |
136 | + | |
137 | + const response = await batchReviewPost({ | |
138 | + params: { owner: "acme", repo: "demo", number: "42" }, | |
139 | + request: new Request("http://localhost/api/repos/acme/demo/pulls/42/reviews/batch", { | |
140 | + method: "POST", | |
141 | + headers: { "content-type": "application/json" }, | |
142 | + body: JSON.stringify({ | |
143 | + state: "COMMENTED", | |
144 | + body: "batch summary", | |
145 | + comments: [{ body: "c1" }, { body: "c2" }], | |
146 | + }), | |
147 | + }), | |
148 | + locals: { user: { id: "reviewer-1" } }, | |
149 | + } as any); | |
150 | + | |
151 | + const body = await readJson(response); | |
152 | + expect(response.status).toBe(200); | |
153 | + expect(body?.success).toBe(true); | |
154 | + expect(body?.data?.commentCount).toBe(2); | |
155 | + | |
156 | + const reviewInserts = mockDb.__state.insertCalls.filter( | |
157 | + (entry: any) => entry.table === fakeSchema.pullRequestReviews | |
158 | + ); | |
159 | + const commentInserts = mockDb.__state.insertCalls.filter( | |
160 | + (entry: any) => entry.table === fakeSchema.pullRequestComments | |
161 | + ); | |
162 | + expect(reviewInserts).toHaveLength(1); | |
163 | + expect(commentInserts).toHaveLength(2); | |
164 | + }); | |
165 | + | |
166 | + it("rolls back created rows when comment insert fails mid-batch", async () => { | |
167 | + mockDb = makeDb({ failCommentInsert: true }); | |
168 | + | |
169 | + const response = await batchReviewPost({ | |
170 | + params: { owner: "acme", repo: "demo", number: "42" }, | |
171 | + request: new Request("http://localhost/api/repos/acme/demo/pulls/42/reviews/batch", { | |
172 | + method: "POST", | |
173 | + headers: { "content-type": "application/json" }, | |
174 | + body: JSON.stringify({ | |
175 | + state: "COMMENTED", | |
176 | + comments: [{ body: "c1" }, { body: "c2" }], | |
177 | + }), | |
178 | + }), | |
179 | + locals: { user: { id: "reviewer-1" } }, | |
180 | + } as any); | |
181 | + | |
182 | + const body = await readJson(response); | |
183 | + expect(response.status).toBe(500); | |
184 | + expect(body?.error?.code).toBe("INTERNAL_ERROR"); | |
185 | + | |
186 | + // One cleanup delete for comments + one for the review record. | |
187 | + expect(mockDb.__state.deleteTables).toContain(fakeSchema.pullRequestComments); | |
188 | + expect(mockDb.__state.deleteTables).toContain(fakeSchema.pullRequestReviews); | |
189 | + }); | |
190 | +}); | |
0 | 191 | |