Skip to content

Commit 4913987

Browse files
authored
Merge pull request #25 from DaleStudy/fix/filter-non-allowed-repos
leetcode-study ์™ธ ์ €์žฅ์†Œ์— Week ๊ฒฝ๊ณ  ๋Œ“๊ธ€์ด ๋‹ฌ๋ฆฌ๋Š” ๋ฒ„๊ทธ ์ˆ˜์ •
2 parents 2129134 + 591ce76 commit 4913987

File tree

5 files changed

+364
-0
lines changed

5 files changed

+364
-0
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Integration
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
steps:
15+
- uses: actions/checkout@v6
16+
- uses: oven-sh/setup-bun@v2
17+
- run: bun test

โ€Žhandlers/check-weeks.jsโ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { generateGitHubAppToken, getGitHubHeaders } from "../utils/github.js";
66
import { corsResponse, errorResponse } from "../utils/cors.js";
77
import { handleWeekComment } from "../utils/prWeeks.js";
88
import { validateOrganization, hasMaintenanceLabel } from "../utils/validation.js";
9+
import { ALLOWED_REPO } from "../utils/constants.js";
910

1011
/**
1112
* ๋ชจ๋“  Open PR์˜ Week ์„ค์ •์„ ๊ฒ€์‚ฌํ•˜๊ณ  ์ž๋™์œผ๋กœ ๋Œ“๊ธ€ ์ž‘์„ฑ/์‚ญ์ œ
@@ -29,6 +30,11 @@ export async function checkWeeks(request, env) {
2930
return errorResponse(`Unauthorized organization: ${repoOwner}`, 403);
3031
}
3132

33+
// ํ—ˆ์šฉ๋œ repository๋งŒ ์ฒ˜๋ฆฌ
34+
if (repo_name !== ALLOWED_REPO) {
35+
return errorResponse(`Unauthorized repository: ${repo_name}`, 403);
36+
}
37+
3238
// GitHub App Token ์ƒ์„ฑ
3339
const appToken = await generateGitHubAppToken(env);
3440

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { describe, it, expect, vi, beforeEach } from "bun:test";
2+
3+
vi.mock("../utils/github.js", () => ({
4+
generateGitHubAppToken: vi.fn().mockResolvedValue("fake-token"),
5+
getGitHubHeaders: vi.fn().mockReturnValue({
6+
Authorization: "token fake-token",
7+
}),
8+
}));
9+
10+
vi.mock("../utils/prWeeks.js", () => ({
11+
handleWeekComment: vi.fn().mockResolvedValue("Week 1"),
12+
}));
13+
14+
import { checkWeeks } from "./check-weeks.js";
15+
import { generateGitHubAppToken } from "../utils/github.js";
16+
17+
function makeRequest(body) {
18+
return new Request("https://example.com/check-weeks", {
19+
method: "POST",
20+
headers: { "Content-Type": "application/json" },
21+
body: JSON.stringify(body),
22+
});
23+
}
24+
25+
const env = {};
26+
27+
describe("check-weeks repo filtering", () => {
28+
beforeEach(() => {
29+
vi.clearAllMocks();
30+
globalThis.fetch = vi.fn().mockResolvedValue({
31+
ok: true,
32+
json: () => Promise.resolve([]),
33+
});
34+
});
35+
36+
it("returns 403 for non-DaleStudy organization", async () => {
37+
const request = makeRequest({
38+
repo_owner: "OtherOrg",
39+
repo_name: "leetcode-study",
40+
});
41+
42+
const response = await checkWeeks(request, env);
43+
expect(response.status).toBe(403);
44+
45+
const body = await response.json();
46+
expect(body.error).toContain("Unauthorized organization");
47+
});
48+
49+
it("returns 403 for non-leetcode-study repo_name", async () => {
50+
const request = makeRequest({
51+
repo_owner: "DaleStudy",
52+
repo_name: "daleui",
53+
});
54+
55+
const response = await checkWeeks(request, env);
56+
expect(response.status).toBe(403);
57+
58+
const body = await response.json();
59+
expect(body.error).toContain("Unauthorized repository");
60+
61+
expect(generateGitHubAppToken).not.toHaveBeenCalled();
62+
});
63+
64+
it("processes leetcode-study repo_name successfully", async () => {
65+
const request = makeRequest({
66+
repo_owner: "DaleStudy",
67+
repo_name: "leetcode-study",
68+
});
69+
70+
const response = await checkWeeks(request, env);
71+
expect(response.status).toBe(200);
72+
73+
const body = await response.json();
74+
expect(body.success).toBe(true);
75+
});
76+
77+
it("returns 400 when repo_name is missing", async () => {
78+
const request = makeRequest({
79+
repo_owner: "DaleStudy",
80+
});
81+
82+
const response = await checkWeeks(request, env);
83+
expect(response.status).toBe(400);
84+
85+
const body = await response.json();
86+
expect(body.error).toContain("repo_name");
87+
});
88+
89+
it("defaults repo_owner to DaleStudy when omitted", async () => {
90+
const request = makeRequest({
91+
repo_name: "leetcode-study",
92+
});
93+
94+
const response = await checkWeeks(request, env);
95+
expect(response.status).toBe(200);
96+
97+
const body = await response.json();
98+
expect(body.success).toBe(true);
99+
});
100+
});

โ€Žhandlers/webhooks.jsโ€Ž

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ async function handleProjectsV2ItemEvent(payload, env) {
125125

126126
const { number: prNumber, owner: repoOwner, repo: repoName } = prInfo;
127127

128+
// ํ—ˆ์šฉ๋œ repository๋งŒ ์ฒ˜๋ฆฌ (projects_v2_item ์ด๋ฒคํŠธ๋Š” payload์— repository๊ฐ€ ์—†์–ด ์ƒ์œ„ ํ•„ํ„ฐ๋ฅผ ์šฐํšŒํ•จ)
129+
if (repoName !== ALLOWED_REPO) {
130+
console.log(`Ignoring projects_v2_item for repository: ${repoName}`);
131+
return corsResponse({ message: `Ignored: ${repoName}` });
132+
}
133+
128134
// PR ์ƒํƒœ ํ™•์ธ (closed PR, maintenance ๋ผ๋ฒจ ์˜ˆ์™ธ)
129135
const prResponse = await fetch(
130136
`https://api.github.com/repos/${repoOwner}/${repoName}/pulls/${prNumber}`,

โ€Žhandlers/webhooks.test.jsโ€Ž

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import { describe, it, expect, vi, beforeEach } from "bun:test";
2+
3+
vi.mock("../utils/github.js", () => ({
4+
generateGitHubAppToken: vi.fn().mockResolvedValue("fake-token"),
5+
getPRInfoFromNodeId: vi.fn(),
6+
getGitHubHeaders: vi.fn().mockReturnValue({
7+
Authorization: "token fake-token",
8+
}),
9+
}));
10+
11+
vi.mock("../utils/prWeeks.js", () => ({
12+
ensureWarningComment: vi.fn().mockResolvedValue(false),
13+
removeWarningComment: vi.fn().mockResolvedValue(false),
14+
handleWeekComment: vi.fn().mockResolvedValue("Week 1"),
15+
}));
16+
17+
vi.mock("../utils/prReview.js", () => ({
18+
performAIReview: vi.fn(),
19+
addReactionToComment: vi.fn(),
20+
}));
21+
22+
vi.mock("../utils/prActions.js", () => ({
23+
hasApprovedReview: vi.fn(),
24+
safeJson: vi.fn(),
25+
}));
26+
27+
vi.mock("./tag-patterns.js", () => ({
28+
tagPatterns: vi.fn(),
29+
}));
30+
31+
vi.mock("./learning-status.js", () => ({
32+
postLearningStatus: vi.fn(),
33+
}));
34+
35+
import { handleWebhook } from "./webhooks.js";
36+
import { getPRInfoFromNodeId } from "../utils/github.js";
37+
import {
38+
ensureWarningComment,
39+
removeWarningComment,
40+
handleWeekComment,
41+
} from "../utils/prWeeks.js";
42+
43+
function makeRequest(eventType, payload) {
44+
return new Request("https://example.com/webhooks", {
45+
method: "POST",
46+
headers: { "X-GitHub-Event": eventType },
47+
body: JSON.stringify(payload),
48+
});
49+
}
50+
51+
const env = {};
52+
53+
describe("webhook repo filtering", () => {
54+
beforeEach(() => {
55+
vi.clearAllMocks();
56+
globalThis.fetch = vi.fn().mockResolvedValue({
57+
ok: true,
58+
json: () =>
59+
Promise.resolve({ state: "open", labels: [], draft: false }),
60+
});
61+
});
62+
63+
describe("top-level filter (payload.repository)", () => {
64+
it("ignores immediately when payload.repository.name is not leetcode-study", async () => {
65+
const request = makeRequest("pull_request", {
66+
action: "opened",
67+
organization: { login: "DaleStudy" },
68+
repository: { name: "daleui", owner: { login: "DaleStudy" } },
69+
pull_request: { number: 1, labels: [], head: { sha: "abc" } },
70+
});
71+
72+
const response = await handleWebhook(request, env);
73+
const body = await response.json();
74+
75+
expect(body.message).toBe("Ignored: daleui");
76+
});
77+
78+
it("passes when payload.repository.name is leetcode-study", async () => {
79+
const request = makeRequest("pull_request", {
80+
action: "synchronize",
81+
organization: { login: "DaleStudy" },
82+
repository: {
83+
name: "leetcode-study",
84+
owner: { login: "DaleStudy" },
85+
},
86+
pull_request: {
87+
number: 1,
88+
labels: [],
89+
head: { sha: "abc" },
90+
user: { login: "testuser" },
91+
},
92+
});
93+
94+
const response = await handleWebhook(request, env);
95+
const body = await response.json();
96+
97+
expect(body.message).toBe("Processed");
98+
});
99+
});
100+
101+
describe("projects_v2_item event repo filtering", () => {
102+
const basePayload = {
103+
action: "edited",
104+
organization: { login: "DaleStudy" },
105+
projects_v2_item: {
106+
content_type: "PullRequest",
107+
content_node_id: "PR_node123",
108+
},
109+
changes: {
110+
field_value: {
111+
field_name: "Week",
112+
to: { title: "Week 1" },
113+
},
114+
},
115+
};
116+
117+
it("ignores when GraphQL lookup returns a non-leetcode-study repo", async () => {
118+
getPRInfoFromNodeId.mockResolvedValue({
119+
number: 962,
120+
owner: "DaleStudy",
121+
repo: "daleui",
122+
});
123+
124+
const request = makeRequest("projects_v2_item", basePayload);
125+
const response = await handleWebhook(request, env);
126+
const body = await response.json();
127+
128+
expect(body.message).toBe("Ignored: daleui");
129+
expect(ensureWarningComment).not.toHaveBeenCalled();
130+
expect(removeWarningComment).not.toHaveBeenCalled();
131+
});
132+
133+
it("processes normally when GraphQL lookup returns leetcode-study", async () => {
134+
getPRInfoFromNodeId.mockResolvedValue({
135+
number: 100,
136+
owner: "DaleStudy",
137+
repo: "leetcode-study",
138+
});
139+
140+
const request = makeRequest("projects_v2_item", basePayload);
141+
const response = await handleWebhook(request, env);
142+
const body = await response.json();
143+
144+
expect(body.message).toBe("Processed");
145+
});
146+
147+
it("ignores non-leetcode-study repo on deleted action", async () => {
148+
getPRInfoFromNodeId.mockResolvedValue({
149+
number: 962,
150+
owner: "DaleStudy",
151+
repo: "daleui",
152+
});
153+
154+
const request = makeRequest("projects_v2_item", {
155+
...basePayload,
156+
action: "deleted",
157+
});
158+
const response = await handleWebhook(request, env);
159+
const body = await response.json();
160+
161+
expect(body.message).toBe("Ignored: daleui");
162+
expect(ensureWarningComment).not.toHaveBeenCalled();
163+
});
164+
165+
it("ignores non-leetcode-study repo on created action", async () => {
166+
getPRInfoFromNodeId.mockResolvedValue({
167+
number: 962,
168+
owner: "DaleStudy",
169+
repo: "daleui",
170+
});
171+
172+
const request = makeRequest("projects_v2_item", {
173+
...basePayload,
174+
action: "created",
175+
});
176+
const response = await handleWebhook(request, env);
177+
const body = await response.json();
178+
179+
expect(body.message).toBe("Ignored: daleui");
180+
expect(handleWeekComment).not.toHaveBeenCalled();
181+
});
182+
});
183+
184+
describe("organization filter", () => {
185+
it("ignores non-DaleStudy organization", async () => {
186+
const request = makeRequest("pull_request", {
187+
action: "opened",
188+
organization: { login: "OtherOrg" },
189+
repository: {
190+
name: "leetcode-study",
191+
owner: { login: "OtherOrg" },
192+
},
193+
pull_request: { number: 1, labels: [], head: { sha: "abc" } },
194+
});
195+
196+
const response = await handleWebhook(request, env);
197+
const body = await response.json();
198+
199+
expect(body.message).toBe("Ignored: not DaleStudy organization");
200+
});
201+
202+
it("ignores when organization field is missing", async () => {
203+
const request = makeRequest("pull_request", {
204+
action: "opened",
205+
repository: {
206+
name: "leetcode-study",
207+
owner: { login: "DaleStudy" },
208+
},
209+
pull_request: { number: 1, labels: [], head: { sha: "abc" } },
210+
});
211+
212+
const response = await handleWebhook(request, env);
213+
const body = await response.json();
214+
215+
expect(body.message).toBe("Ignored: not DaleStudy organization");
216+
});
217+
});
218+
219+
describe("event type filter", () => {
220+
it("ignores unsupported event types", async () => {
221+
const request = makeRequest("push", {
222+
organization: { login: "DaleStudy" },
223+
repository: {
224+
name: "leetcode-study",
225+
owner: { login: "DaleStudy" },
226+
},
227+
});
228+
229+
const response = await handleWebhook(request, env);
230+
const body = await response.json();
231+
232+
expect(body.message).toBe("Ignored: push");
233+
});
234+
});
235+
});

0 commit comments

Comments
ย (0)