Skip to content

Commit 4d50773

Browse files
Merge pull request #3884 from verifywise-ai/mo-346-may-11-BE-services-test
More unit test coverage on the `/services` in backend
2 parents 3cc2f02 + 17b33bf commit 4d50773

13 files changed

Lines changed: 2431 additions & 0 deletions
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
/**
2+
* @fileoverview GitHub Status Reporter Tests
3+
*
4+
* Tests for posting scan results to GitHub commit status and PR comments.
5+
*
6+
* @module tests/githubStatusReporter
7+
*/
8+
9+
jest.mock("../../../utils/githubToken.utils", () => ({
10+
getDecryptedGitHubToken: jest.fn(),
11+
}));
12+
13+
jest.mock("../../../utils/aiDetectionRepository.utils", () => ({
14+
getRepositoryByIdQuery: jest.fn(),
15+
}));
16+
17+
jest.mock("../../../utils/aiDetection.utils", () => ({
18+
getFindingsSummaryQuery: jest.fn(),
19+
}));
20+
21+
jest.mock("../../../utils/logger/fileLogger", () => ({
22+
default: { info: jest.fn(), warn: jest.fn(), error: jest.fn() },
23+
__esModule: true,
24+
}));
25+
26+
import https from "https";
27+
import { reportScanToGitHub } from "../githubStatusReporter";
28+
import { getDecryptedGitHubToken } from "../../../utils/githubToken.utils";
29+
import { getRepositoryByIdQuery } from "../../../utils/aiDetectionRepository.utils";
30+
import { getFindingsSummaryQuery } from "../../../utils/aiDetection.utils";
31+
32+
const mockGetToken = getDecryptedGitHubToken as jest.MockedFunction<typeof getDecryptedGitHubToken>;
33+
const mockGetRepo = getRepositoryByIdQuery as jest.MockedFunction<typeof getRepositoryByIdQuery>;
34+
const mockGetSummary = getFindingsSummaryQuery as jest.MockedFunction<
35+
typeof getFindingsSummaryQuery
36+
>;
37+
38+
describe("githubStatusReporter", () => {
39+
let requestMock: any;
40+
let responseMock: any;
41+
42+
beforeEach(() => {
43+
jest.clearAllMocks();
44+
45+
responseMock = {
46+
on: jest.fn().mockImplementation((event: string, handler: any) => {
47+
if (event === "data") {
48+
setTimeout(() => handler('{"id":1}'), 0);
49+
}
50+
if (event === "end") {
51+
setTimeout(() => handler(), 10);
52+
}
53+
}),
54+
statusCode: 201,
55+
};
56+
57+
requestMock = {
58+
on: jest.fn(),
59+
write: jest.fn(),
60+
end: jest.fn(),
61+
};
62+
63+
jest.spyOn(https, "request").mockImplementation((_options: any, callback: any) => {
64+
if (callback) setTimeout(() => callback(responseMock), 0);
65+
return requestMock as any;
66+
});
67+
});
68+
69+
afterEach(() => {
70+
jest.restoreAllMocks();
71+
});
72+
73+
describe("reportScanToGitHub", () => {
74+
it("should skip non-webhook scans", async () => {
75+
await reportScanToGitHub({ trigger_type: "manual" } as any, 1);
76+
expect(mockGetToken).not.toHaveBeenCalled();
77+
});
78+
79+
it("should skip when commit_sha is missing", async () => {
80+
await reportScanToGitHub({ trigger_type: "webhook", repository_id: 1 } as any, 1);
81+
expect(mockGetToken).not.toHaveBeenCalled();
82+
});
83+
84+
it("should skip when repository_id is missing", async () => {
85+
await reportScanToGitHub({ trigger_type: "webhook", commit_sha: "abc" } as any, 1);
86+
expect(mockGetToken).not.toHaveBeenCalled();
87+
});
88+
89+
it("should skip when repository not found", async () => {
90+
mockGetRepo.mockResolvedValue(null);
91+
await reportScanToGitHub(
92+
{ trigger_type: "webhook", commit_sha: "abc", repository_id: 1 } as any,
93+
1,
94+
);
95+
expect(mockGetToken).not.toHaveBeenCalled();
96+
});
97+
98+
it("should skip when no GitHub token exists", async () => {
99+
mockGetRepo.mockResolvedValue({ id: 1 } as any);
100+
mockGetToken.mockResolvedValue(null);
101+
await reportScanToGitHub(
102+
{ trigger_type: "webhook", commit_sha: "abc", repository_id: 1 } as any,
103+
1,
104+
);
105+
expect(https.request).not.toHaveBeenCalled();
106+
});
107+
108+
it("should post success status when thresholds pass", async () => {
109+
mockGetRepo.mockResolvedValue({
110+
id: 1,
111+
ci_status_checks: true,
112+
ci_post_comments: false,
113+
ci_min_score: 80,
114+
ci_max_critical: 5,
115+
} as any);
116+
mockGetToken.mockResolvedValue("ghp_token");
117+
118+
await reportScanToGitHub(
119+
{
120+
id: 1,
121+
trigger_type: "webhook",
122+
commit_sha: "abc123",
123+
repository_id: 1,
124+
repository_owner: "owner",
125+
repository_name: "repo",
126+
status: "completed",
127+
risk_score: 50,
128+
findings_count: 2,
129+
} as any,
130+
1,
131+
);
132+
133+
expect(https.request).toHaveBeenCalled();
134+
const options = (https.request as jest.MockedFunction<typeof https.request>).mock.calls[0][0];
135+
expect(options.path).toContain("/statuses/abc123");
136+
});
137+
138+
it("should post failure status when risk score exceeds threshold", async () => {
139+
mockGetRepo.mockResolvedValue({
140+
id: 1,
141+
ci_status_checks: true,
142+
ci_post_comments: false,
143+
ci_min_score: 80,
144+
ci_max_critical: 5,
145+
} as any);
146+
mockGetToken.mockResolvedValue("ghp_token");
147+
148+
await reportScanToGitHub(
149+
{
150+
id: 1,
151+
trigger_type: "webhook",
152+
commit_sha: "abc123",
153+
repository_id: 1,
154+
repository_owner: "owner",
155+
repository_name: "repo",
156+
status: "completed",
157+
risk_score: 90,
158+
findings_count: 2,
159+
} as any,
160+
1,
161+
);
162+
163+
const postData = JSON.parse(requestMock.write.mock.calls[0][0]);
164+
expect(postData.state).toBe("failure");
165+
});
166+
167+
it("should post error status when scan failed", async () => {
168+
mockGetRepo.mockResolvedValue({
169+
id: 1,
170+
ci_status_checks: true,
171+
ci_post_comments: false,
172+
ci_min_score: 80,
173+
ci_max_critical: 5,
174+
} as any);
175+
mockGetToken.mockResolvedValue("ghp_token");
176+
177+
await reportScanToGitHub(
178+
{
179+
id: 1,
180+
trigger_type: "webhook",
181+
commit_sha: "abc123",
182+
repository_id: 1,
183+
repository_owner: "owner",
184+
repository_name: "repo",
185+
status: "failed",
186+
risk_score: 50,
187+
findings_count: 2,
188+
} as any,
189+
1,
190+
);
191+
192+
const postData = JSON.parse(requestMock.write.mock.calls[0][0]);
193+
expect(postData.state).toBe("error");
194+
});
195+
196+
it("should post PR comment when enabled and pr_number exists", async () => {
197+
mockGetRepo.mockResolvedValue({
198+
id: 1,
199+
ci_status_checks: false,
200+
ci_post_comments: true,
201+
ci_min_score: 80,
202+
ci_max_critical: 5,
203+
} as any);
204+
mockGetToken.mockResolvedValue("ghp_token");
205+
mockGetSummary.mockResolvedValue({
206+
by_finding_type: { secret: 2, vulnerability: 1 },
207+
} as any);
208+
209+
await reportScanToGitHub(
210+
{
211+
id: 1,
212+
trigger_type: "webhook",
213+
commit_sha: "abc123",
214+
repository_id: 1,
215+
repository_owner: "owner",
216+
repository_name: "repo",
217+
pr_number: 42,
218+
status: "completed",
219+
risk_score: 50,
220+
findings_count: 2,
221+
files_scanned: 10,
222+
scan_mode: "full",
223+
duration_ms: 5000,
224+
risk_score_grade: "B",
225+
} as any,
226+
1,
227+
);
228+
229+
const calls = (https.request as jest.MockedFunction<typeof https.request>).mock.calls;
230+
const prCall = calls.find((c) => (c[0] as any).path?.includes("/issues/42/comments"));
231+
expect(prCall).toBeDefined();
232+
});
233+
234+
it("should not post PR comment when pr_number is missing", async () => {
235+
mockGetRepo.mockResolvedValue({
236+
id: 1,
237+
ci_status_checks: true,
238+
ci_post_comments: true,
239+
ci_min_score: 80,
240+
ci_max_critical: 5,
241+
} as any);
242+
mockGetToken.mockResolvedValue("ghp_token");
243+
244+
await reportScanToGitHub(
245+
{
246+
id: 1,
247+
trigger_type: "webhook",
248+
commit_sha: "abc123",
249+
repository_id: 1,
250+
repository_owner: "owner",
251+
repository_name: "repo",
252+
status: "completed",
253+
risk_score: 50,
254+
findings_count: 2,
255+
} as any,
256+
1,
257+
);
258+
259+
const calls = (https.request as jest.MockedFunction<typeof https.request>).mock.calls;
260+
const paths = calls.map((c) => (c[0] as any).path);
261+
expect(paths.some((p) => p?.includes("/issues/"))).toBe(false);
262+
});
263+
264+
it("should handle GitHub API errors gracefully", async () => {
265+
responseMock.statusCode = 422;
266+
mockGetRepo.mockResolvedValue({
267+
id: 1,
268+
ci_status_checks: true,
269+
ci_post_comments: false,
270+
ci_min_score: 80,
271+
ci_max_critical: 5,
272+
} as any);
273+
mockGetToken.mockResolvedValue("ghp_token");
274+
275+
await reportScanToGitHub(
276+
{
277+
id: 1,
278+
trigger_type: "webhook",
279+
commit_sha: "abc123",
280+
repository_id: 1,
281+
repository_owner: "owner",
282+
repository_name: "repo",
283+
status: "completed",
284+
risk_score: 50,
285+
findings_count: 2,
286+
} as any,
287+
1,
288+
);
289+
290+
// Should not throw; error is logged
291+
expect(https.request).toHaveBeenCalled();
292+
});
293+
294+
it("should handle getFindingsSummary error gracefully", async () => {
295+
mockGetRepo.mockResolvedValue({
296+
id: 1,
297+
ci_status_checks: false,
298+
ci_post_comments: true,
299+
ci_min_score: 80,
300+
ci_max_critical: 5,
301+
} as any);
302+
mockGetToken.mockResolvedValue("ghp_token");
303+
mockGetSummary.mockRejectedValue(new Error("DB error"));
304+
305+
await reportScanToGitHub(
306+
{
307+
id: 1,
308+
trigger_type: "webhook",
309+
commit_sha: "abc123",
310+
repository_id: 1,
311+
repository_owner: "owner",
312+
repository_name: "repo",
313+
pr_number: 42,
314+
status: "completed",
315+
risk_score: 50,
316+
findings_count: 2,
317+
} as any,
318+
1,
319+
);
320+
321+
const prCall = (https.request as jest.MockedFunction<typeof https.request>).mock.calls.find(
322+
(c) => (c[0] as any).path?.includes("/issues/42/comments"),
323+
);
324+
expect(prCall).toBeDefined();
325+
});
326+
});
327+
});

0 commit comments

Comments
 (0)