Add stack merge-readiness API and OpenAPI docs

swadhinbiswas committed 8 months ago
d6cd63a
1 parent 4187d66
commit d6cd63a
3 files changed
+151 -0
▼src/lib/openapi.ts
+18-0
505
505
},
506
506
},
507
507
},
508
+ "/repos/{owner}/{repo}/stacks/{stackId}/merge-readiness": {
509
+ get: {
510
+ tags: ["Stacks"],
511
+ summary: "Get stack merge readiness",
512
+ description: "Returns whether the full stack can be merged now, including blocking approval reasons.",
513
+ security: [{ bearerAuth: [] }],
514
+ parameters: [
515
+ { name: "owner", in: "path", required: true, schema: { type: "string" } },
516
+ { name: "repo", in: "path", required: true, schema: { type: "string" } },
517
+ { name: "stackId", in: "path", required: true, schema: { type: "string" } },
518
+ ],
519
+ responses: {
520
+ 200: { description: "Merge readiness returned" },
521
+ 401: { description: "Unauthorized", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } },
522
+ 404: { description: "Repository or stack not found", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } },
523
+ },
524
+ },
525
+ },
508
526
"/repos/{owner}/{repo}/pulls/{number}/comments": {
509
527
get: {
510
528
tags: ["Pull Requests"],
511
529
▼src/pages/api/repos/[owner]/[repo]/stacks/[stackId]/merge-readiness.ts
+51-0
1
+import type { APIRoute } from "astro";
2
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
3
+import { and, eq } from "drizzle-orm";
4
+import { getDatabase, schema } from "@/db";
5
+import { canReadRepo } from "@/lib/permissions";
6
+import { withErrorHandler } from "@/lib/errors";
7
+import { badRequest, notFound, success, unauthorized } from "@/lib/api";
8
+import { canMergeStack, getStackApprovalStatus } from "@/lib/stack-approvals";
9
+
10
+export const GET: APIRoute = withErrorHandler(async ({ params, locals }) => {
11
+ const { owner: ownerName, repo: repoName, stackId } = params;
12
+ const user = locals.user;
13
+
14
+ if (!user) return unauthorized();
15
+ if (!ownerName || !repoName || !stackId) return badRequest("Missing parameters");
16
+
17
+ const db = getDatabase() as NodePgDatabase<typeof schema>;
18
+
19
+ const owner = await db.query.users.findFirst({
20
+ where: eq(schema.users.username, ownerName),
21
+ });
22
+ if (!owner) return notFound("Repository not found");
23
+
24
+ const repo = await db.query.repositories.findFirst({
25
+ where: and(
26
+ eq(schema.repositories.ownerId, owner.id),
27
+ eq(schema.repositories.name, repoName)
28
+ ),
29
+ });
30
+ if (!repo) return notFound("Repository not found");
31
+ if (!(await canReadRepo(user.id, repo))) return notFound("Repository not found");
32
+
33
+ const stack = await db.query.prStacks.findFirst({
34
+ where: and(
35
+ eq(schema.prStacks.id, stackId),
36
+ eq(schema.prStacks.repositoryId, repo.id)
37
+ ),
38
+ });
39
+ if (!stack) return notFound("Stack not found");
40
+
41
+ const readiness = await canMergeStack(stackId);
42
+ const approvalStatus = await getStackApprovalStatus(stackId);
43
+
44
+ return success({
45
+ stackId,
46
+ canMerge: readiness.canMerge,
47
+ blockers: readiness.blockers,
48
+ approvalStatus,
49
+ });
50
+});
51
+
0
52
▼tests/integration/stack-merge-readiness-route.test.ts
+82-0
1
+import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+const {
4
+ canReadRepoMock,
5
+ canMergeStackMock,
6
+ getStackApprovalStatusMock,
7
+ fakeSchema,
8
+} = vi.hoisted(() => ({
9
+ canReadRepoMock: vi.fn(async () => true),
10
+ canMergeStackMock: vi.fn(async () => ({ canMerge: true, blockers: [] })),
11
+ getStackApprovalStatusMock: vi.fn(async () => ({ stackId: "stack-1", allApproved: true, prs: [] })),
12
+ fakeSchema: {
13
+ users: { username: {} },
14
+ repositories: { ownerId: {}, name: {}, id: {} },
15
+ prStacks: { id: {}, repositoryId: {} },
16
+ } as any,
17
+}));
18
+
19
+let mockDb: any;
20
+
21
+vi.mock("@/db", () => ({
22
+ getDatabase: () => mockDb,
23
+ schema: fakeSchema,
24
+}));
25
+
26
+vi.mock("@/lib/permissions", () => ({
27
+ canReadRepo: canReadRepoMock,
28
+}));
29
+
30
+vi.mock("@/lib/stack-approvals", () => ({
31
+ canMergeStack: canMergeStackMock,
32
+ getStackApprovalStatus: getStackApprovalStatusMock,
33
+}));
34
+
35
+import { GET as getMergeReadiness } from "@/pages/api/repos/[owner]/[repo]/stacks/[stackId]/merge-readiness";
36
+
37
+function makeDb() {
38
+ return {
39
+ query: {
40
+ users: {
41
+ findFirst: vi.fn(async () => ({ id: "owner-1" })),
42
+ },
43
+ repositories: {
44
+ findFirst: vi.fn(async () => ({ id: "repo-1", ownerId: "owner-1", name: "demo" })),
45
+ },
46
+ prStacks: {
47
+ findFirst: vi.fn(async () => ({ id: "stack-1", repositoryId: "repo-1" })),
48
+ },
49
+ },
50
+ };
51
+}
52
+
53
+async function readJson(response: Response): Promise<any> {
54
+ return response.json();
55
+}
56
+
57
+describe("stack merge readiness route", () => {
58
+ beforeEach(() => {
59
+ mockDb = makeDb();
60
+ canReadRepoMock.mockResolvedValue(true);
61
+ canMergeStackMock.mockResolvedValue({ canMerge: true, blockers: [] });
62
+ getStackApprovalStatusMock.mockResolvedValue({ stackId: "stack-1", allApproved: true, prs: [] });
63
+ });
64
+
65
+ it("returns readiness and blockers for authorized requests", async () => {
66
+ canMergeStackMock.mockResolvedValueOnce({
67
+ canMerge: false,
68
+ blockers: ["PR #12: Needs 1 more approval(s)"],
69
+ });
70
+
71
+ const response = await getMergeReadiness({
72
+ params: { owner: "owner-1", repo: "demo", stackId: "stack-1" },
73
+ locals: { user: { id: "user-1" } },
74
+ } as any);
75
+
76
+ const body = await readJson(response);
77
+ expect(response.status).toBe(200);
78
+ expect(body?.data?.canMerge).toBe(false);
79
+ expect(body?.data?.blockers).toEqual(["PR #12: Needs 1 more approval(s)"]);
80
+ });
81
+});
82
+
0
83