Add integration tests for batch review route behavior

swadhinbiswas committed 8 months ago
54e9ce2
1 parent 742a0c1
commit 54e9ce2
1 file changed
+190 -0
▼tests/integration/review-batch-route.test.ts
+190-0
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