Skip to content
File

Blob: tests/worker/webhooks/public/github.test.ts

typescript210 lines
1import { describe, expect, it } from "vitest";
2 
3import { readProjectDoRows } from "../../../helpers/runtime";
4import { registerWorkerRuntimeHooks } from "../../../helpers/worker-hooks";
5 
6import {
7 buildGitHubRepository,
8 createOwnedProjectContext,
9 getWebhooks,
10 postPublicWebhook,
11 putWebhook,
12 signGitHubPayload,
13} from "../helpers";
14import { AFTER_SHA, BEFORE_SHA } from "../public-helpers";
15 
16describe("webhook public routes", () => {
17 registerWorkerRuntimeHooks();
18 
19 describe("GitHub deliveries", () => {
20 it("accepts verified GitHub pushes and replays duplicates without creating a second run", async () => {
21 const context = await createOwnedProjectContext({
22 user: {
23 email: "webhook-public-github@example.com",
24 slug: "webhook-public-github",
25 },
26 project: {
27 projectSlug: "github-public-project",
28 repoUrl: "https://github.com/example/github-public-project",
29 },
30 });
31 
32 const createWebhook = await putWebhook(context.sessionId, context.project.id, "github", {
33 enabled: true,
34 secret: "github-secret",
35 });
36 expect([200, 201]).toContain(createWebhook.status);
37 
38 const body = JSON.stringify({
39 ref: "refs/heads/main",
40 before: BEFORE_SHA,
41 after: AFTER_SHA,
42 head_commit: {
43 id: AFTER_SHA,
44 },
45 repository: buildGitHubRepository(context.project.repoUrl, context.project.defaultBranch),
46 });
47 
48 const headers = {
49 "content-type": "application/json; charset=utf-8",
50 "x-github-event": "push",
51 "x-github-delivery": "github-delivery-accepted",
52 "x-hub-signature-256": await signGitHubPayload("github-secret", body),
53 };
54 
55 const accepted = await postPublicWebhook("github", context.project, body, headers);
56 expect(accepted.status).toBe(202);
57 
58 const duplicated = await postPublicWebhook("github", context.project, body, headers);
59 expect(duplicated.status).toBe(200);
60 
61 const rows = await readProjectDoRows(context.project.id);
62 expect(rows.runs).toHaveLength(1);
63 expect(rows.runs[0]?.triggerType).toBe("webhook");
64 expect(rows.runs[0]?.provider).toBe("github");
65 expect(rows.runs[0]?.deliveryId).toBe("github-delivery-accepted");
66 expect(rows.runs[0]?.branch).toBe("main");
67 expect(rows.runs[0]?.commitSha).toBe(AFTER_SHA);
68 expect(rows.webhookDeliveries).toHaveLength(1);
69 expect(rows.webhookDeliveries[0]?.outcome).toBe("accepted");
70 
71 const listed = await getWebhooks(context.sessionId, context.project.id);
72 expect(listed.status).toBe(200);
73 expect(listed.body?.webhooks).toHaveLength(1);
74 expect(listed.body?.webhooks[0]?.recentDeliveries).toHaveLength(1);
75 expect(listed.body?.webhooks[0]?.recentDeliveries[0]).toMatchObject({
76 provider: "github",
77 deliveryId: "github-delivery-accepted",
78 outcome: "accepted",
79 branch: "main",
80 commitSha: AFTER_SHA,
81 });
82 });
83 
84 it("accepts GitHub pushes when the stored repo URL casing differs from the delivery payload", async () => {
85 const context = await createOwnedProjectContext({
86 user: {
87 email: "webhook-public-github-mixed-case@example.com",
88 slug: "webhook-public-github-mixed-case",
89 },
90 project: {
91 projectSlug: "github-mixed-case-project",
92 repoUrl: "https://github.com/Example/Mixed-Case-Project",
93 },
94 });
95 
96 await putWebhook(context.sessionId, context.project.id, "github", {
97 enabled: true,
98 secret: "github-mixed-case-secret",
99 });
100 
101 const payloadRepoUrl = "https://github.com/example/mixed-case-project";
102 const body = JSON.stringify({
103 ref: "refs/heads/main",
104 before: BEFORE_SHA,
105 after: AFTER_SHA,
106 repository: buildGitHubRepository(payloadRepoUrl, context.project.defaultBranch),
107 });
108 
109 const accepted = await postPublicWebhook("github", context.project, body, {
110 "content-type": "application/json; charset=utf-8",
111 "x-github-event": "push",
112 "x-github-delivery": "github-mixed-case-delivery",
113 "x-hub-signature-256": await signGitHubPayload("github-mixed-case-secret", body),
114 });
115 expect(accepted.status).toBe(202);
116 
117 const rows = await readProjectDoRows(context.project.id);
118 expect(rows.runs).toHaveLength(1);
119 expect(rows.runs[0]?.deliveryId).toBe("github-mixed-case-delivery");
120 expect(rows.webhookDeliveries).toHaveLength(1);
121 expect(rows.webhookDeliveries[0]?.outcome).toBe("accepted");
122 expect(rows.webhookDeliveries[0]?.repoUrl).toBe(payloadRepoUrl);
123 
124 const listed = await getWebhooks(context.sessionId, context.project.id);
125 expect(listed.status).toBe(200);
126 expect(listed.body?.webhooks[0]?.recentDeliveries[0]?.repoUrl).toBe(payloadRepoUrl);
127 });
128 
129 it("records ignored GitHub events, ignored branches, and caps recent deliveries at ten", async () => {
130 const context = await createOwnedProjectContext({
131 user: {
132 email: "webhook-public-github-ignored@example.com",
133 slug: "webhook-public-github-ignored",
134 },
135 project: {
136 projectSlug: "github-ignored-project",
137 repoUrl: "https://github.com/example/github-ignored-project",
138 },
139 });
140 
141 await putWebhook(context.sessionId, context.project.id, "github", {
142 enabled: true,
143 secret: "github-ignored-secret",
144 });
145 
146 const issuesBody = JSON.stringify({
147 action: "opened",
148 issue: {
149 number: 1,
150 },
151 repository: buildGitHubRepository(context.project.repoUrl, context.project.defaultBranch),
152 });
153 const issuesResponse = await postPublicWebhook("github", context.project, issuesBody, {
154 "content-type": "application/json; charset=utf-8",
155 "x-github-event": "issues",
156 "x-github-delivery": "github-ignored-event",
157 "x-hub-signature-256": await signGitHubPayload("github-ignored-secret", issuesBody),
158 });
159 expect(issuesResponse.status).toBe(200);
160 
161 const branchBody = JSON.stringify({
162 ref: "refs/heads/feature-x",
163 before: BEFORE_SHA,
164 after: AFTER_SHA,
165 head_commit: {
166 id: AFTER_SHA,
167 },
168 repository: buildGitHubRepository(context.project.repoUrl, context.project.defaultBranch),
169 });
170 const branchResponse = await postPublicWebhook("github", context.project, branchBody, {
171 "content-type": "application/json; charset=utf-8",
172 "x-github-event": "push",
173 "x-github-delivery": "github-ignored-branch",
174 "x-hub-signature-256": await signGitHubPayload("github-ignored-secret", branchBody),
175 });
176 expect(branchResponse.status).toBe(200);
177 
178 for (let index = 0; index < 12; index += 1) {
179 const pingBody = JSON.stringify({
180 zen: "keep it logically awesome",
181 repository: buildGitHubRepository(context.project.repoUrl, context.project.defaultBranch),
182 });
183 const pingResponse = await postPublicWebhook("github", context.project, pingBody, {
184 "content-type": "application/json; charset=utf-8",
185 "x-github-event": "ping",
186 "x-github-delivery": `github-ping-${index.toString().padStart(2, "0")}`,
187 "x-hub-signature-256": await signGitHubPayload("github-ignored-secret", pingBody),
188 });
189 expect(pingResponse.status).toBe(200);
190 await new Promise((resolve) => setTimeout(resolve, 2));
191 }
192 
193 const rows = await readProjectDoRows(context.project.id);
194 expect(rows.runs).toHaveLength(0);
195 expect(rows.webhookDeliveries).toHaveLength(14);
196 expect(rows.webhookDeliveries.some((row) => row.outcome === "ignored_event")).toBe(true);
197 expect(rows.webhookDeliveries.some((row) => row.outcome === "ignored_branch")).toBe(true);
198 expect(rows.webhookDeliveries.filter((row) => row.outcome === "ignored_ping")).toHaveLength(12);
199 
200 const listed = await getWebhooks(context.sessionId, context.project.id);
201 expect(listed.status).toBe(200);
202 expect(listed.body?.webhooks).toHaveLength(1);
203 expect(listed.body?.webhooks[0]?.recentDeliveries).toHaveLength(10);
204 expect(listed.body?.webhooks[0]?.recentDeliveries.every((delivery) => delivery.outcome === "ignored_ping")).toBe(
205 true,
206 );
207 });
208 });
209});