Skip to content

Commit 03d2f5f

Browse files
authored
feat: import config
1 parent 62d947f commit 03d2f5f

File tree

14 files changed

+1555
-42
lines changed

14 files changed

+1555
-42
lines changed

package-lock.json

Lines changed: 12 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@codacy/codacy-cloud-cli",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"description": "A command-line tool to interact with Codacy Cloud from your terminal",
55
"homepage": "https://www.codacy.com",
66
"repository": {
@@ -40,9 +40,10 @@
4040
"author": "Codacy <support@codacy.com> (https://www.codacy.com)",
4141
"license": "ISC",
4242
"engines": {
43-
"node": ">=18"
43+
"node": ">=20"
4444
},
4545
"dependencies": {
46+
"@codacy/tooling": "0.1.0",
4647
"ansis": "4.0.0",
4748
"cli-table3": "^0.6.3",
4849
"commander": "14.0.0",

src/commands/findings.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,117 @@ describe("findings command", () => {
500500
);
501501
});
502502

503+
it("should pass a custom limit <= 100 directly to the API", async () => {
504+
vi.mocked(SecurityService.searchSecurityItems).mockResolvedValue({
505+
data: [],
506+
} as any);
507+
508+
const program = createProgram();
509+
await program.parseAsync([
510+
"node",
511+
"test",
512+
"findings",
513+
"gh",
514+
"test-org",
515+
"test-repo",
516+
"--limit",
517+
"50",
518+
]);
519+
520+
expect(SecurityService.searchSecurityItems).toHaveBeenCalledWith(
521+
"gh",
522+
"test-org",
523+
undefined,
524+
50,
525+
"Status",
526+
"asc",
527+
{
528+
repositories: ["test-repo"],
529+
statuses: ["Overdue", "OnTrack", "DueSoon"],
530+
},
531+
);
532+
});
533+
534+
it("should paginate when limit > 100", async () => {
535+
const page1 = Array.from({ length: 100 }, (_, i) => ({
536+
id: `finding-${i}`,
537+
title: `Finding ${i}`,
538+
priority: "Medium",
539+
status: "OnTrack",
540+
securityCategory: "Other",
541+
scanType: "SAST",
542+
dueAt: "2024-06-01T00:00:00Z",
543+
}));
544+
const page2 = Array.from({ length: 50 }, (_, i) => ({
545+
id: `finding-${100 + i}`,
546+
title: `Finding ${100 + i}`,
547+
priority: "Medium",
548+
status: "OnTrack",
549+
securityCategory: "Other",
550+
scanType: "SAST",
551+
dueAt: "2024-06-01T00:00:00Z",
552+
}));
553+
554+
vi.mocked(SecurityService.searchSecurityItems)
555+
.mockResolvedValueOnce({
556+
data: page1,
557+
pagination: { cursor: "cursor-2", limit: 100, total: 300 },
558+
} as any)
559+
.mockResolvedValueOnce({
560+
data: page2,
561+
pagination: { cursor: undefined, limit: 100, total: 300 },
562+
} as any);
563+
564+
const program = createProgram();
565+
await program.parseAsync([
566+
"node",
567+
"test",
568+
"findings",
569+
"gh",
570+
"test-org",
571+
"test-repo",
572+
"--limit",
573+
"200",
574+
]);
575+
576+
expect(SecurityService.searchSecurityItems).toHaveBeenCalledTimes(2);
577+
expect(SecurityService.searchSecurityItems).toHaveBeenNthCalledWith(
578+
1, "gh", "test-org", undefined, 100, "Status", "asc",
579+
{ repositories: ["test-repo"], statuses: ["Overdue", "OnTrack", "DueSoon"] },
580+
);
581+
expect(SecurityService.searchSecurityItems).toHaveBeenNthCalledWith(
582+
2, "gh", "test-org", "cursor-2", 100, "Status", "asc",
583+
{ repositories: ["test-repo"], statuses: ["Overdue", "OnTrack", "DueSoon"] },
584+
);
585+
586+
const output = getAllOutput();
587+
expect(output).toContain("Findings — Found 300 findings");
588+
});
589+
590+
it("should cap limit at 1000", async () => {
591+
vi.mocked(SecurityService.searchSecurityItems).mockResolvedValue({
592+
data: [],
593+
} as any);
594+
595+
const program = createProgram();
596+
await program.parseAsync([
597+
"node",
598+
"test",
599+
"findings",
600+
"gh",
601+
"test-org",
602+
"test-repo",
603+
"--limit",
604+
"5000",
605+
]);
606+
607+
// Should use pageSize 100 (min of 1000, 100)
608+
expect(SecurityService.searchSecurityItems).toHaveBeenCalledWith(
609+
"gh", "test-org", undefined, 100, "Status", "asc",
610+
{ repositories: ["test-repo"], statuses: ["Overdue", "OnTrack", "DueSoon"] },
611+
);
612+
});
613+
503614
it("should fail when CODACY_API_TOKEN is not set", async () => {
504615
delete process.env.CODACY_API_TOKEN;
505616

src/commands/findings.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ export function registerFindingsCommand(program: Command) {
176176
"-T, --scan-types <types>",
177177
"comma-separated scan types (case-insensitive): SAST, Secrets, SCA, CICD, IaC, DAST, PenTesting, License, CSPM",
178178
)
179+
.option("-n, --limit <n>", "maximum number of findings to return (default: 100, max: 1000)", "100")
179180
.option("-d, --dast-targets <urls>", "comma-separated DAST target URLs")
180181
.addHelpText(
181182
"after",
@@ -185,6 +186,7 @@ Examples:
185186
$ codacy findings gh my-org
186187
$ codacy findings gh my-org --severities Critical,High
187188
$ codacy findings gh my-org my-repo --statuses Overdue,DueSoon
189+
$ codacy findings gh my-org my-repo --limit 500
188190
$ codacy findings gh my-org my-repo --output json`,
189191
)
190192
.action(async function (
@@ -214,25 +216,38 @@ Examples:
214216
const dastTargets = parseCommaList(opts.dastTargets);
215217
if (dastTargets) body.dastTargetUrls = dastTargets;
216218

219+
const limit = Math.min(Math.max(parseInt(opts.limit, 10) || 100, 1), 1000);
220+
217221
const spinner = ora(
218222
repository
219223
? "Fetching findings..."
220224
: "Fetching organization findings...",
221225
).start();
222226

223-
const response = await SecurityService.searchSecurityItems(
224-
provider,
225-
organization,
226-
undefined,
227-
100,
228-
"Status", // actually sorting by due date
229-
"asc",
230-
body,
231-
);
232-
spinner.stop();
227+
const pageSize = Math.min(limit, 100);
228+
let items: SrmItem[] = [];
229+
let cursor: string | undefined;
230+
let total: number | undefined;
233231

234-
const items = response.data;
235-
const total = response.pagination?.total ?? items.length;
232+
do {
233+
const response = await SecurityService.searchSecurityItems(
234+
provider,
235+
organization,
236+
cursor,
237+
pageSize,
238+
"Status", // actually sorting by due date
239+
"asc",
240+
body,
241+
);
242+
items = items.concat(response.data);
243+
total ??= response.pagination?.total;
244+
cursor = response.pagination?.cursor;
245+
} while (cursor && items.length < limit);
246+
247+
// Trim to exact limit
248+
if (items.length > limit) items = items.slice(0, limit);
249+
total ??= items.length;
250+
spinner.stop();
236251

237252
if (format === "json") {
238253
printJson({
@@ -261,10 +276,12 @@ Examples:
261276

262277
// Show repository column only when browsing org-wide (no repo filter)
263278
printFindingsList(items, total, !repository);
264-
printPaginationWarning(
265-
response.pagination,
266-
"Use --severities or --statuses to filter findings.",
267-
);
279+
if (total > items.length) {
280+
printPaginationWarning(
281+
{ cursor: "more", limit: items.length },
282+
"Use --limit <n> (max 1000) to fetch more, or --severities, --statuses to filter.",
283+
);
284+
}
268285
} catch (err) {
269286
handleError(err);
270287
}

src/commands/issues.test.ts

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -470,9 +470,9 @@ describe("issues command", () => {
470470
"test-repo",
471471
]);
472472

473-
expect(console.log).toHaveBeenCalledWith(
474-
expect.stringContaining('"Potential SQL injection vulnerability"'),
475-
);
473+
const jsonOutput = (console.log as ReturnType<typeof vi.fn>).mock.calls[0][0];
474+
expect(jsonOutput).toContain('"Potential SQL injection vulnerability"');
475+
expect(jsonOutput).toContain('"sql-injection"');
476476
});
477477

478478
it("should output JSON for overview when --overview --output json is specified", async () => {
@@ -498,6 +498,120 @@ describe("issues command", () => {
498498
);
499499
});
500500

501+
it("should pass a custom limit <= 100 directly to the API", async () => {
502+
vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({
503+
data: [],
504+
} as any);
505+
506+
const program = createProgram();
507+
await program.parseAsync([
508+
"node",
509+
"test",
510+
"issues",
511+
"gh",
512+
"test-org",
513+
"test-repo",
514+
"--limit",
515+
"50",
516+
]);
517+
518+
expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith(
519+
"gh",
520+
"test-org",
521+
"test-repo",
522+
undefined,
523+
50,
524+
{},
525+
);
526+
});
527+
528+
it("should paginate when limit > 100", async () => {
529+
const page1Issues = Array.from({ length: 100 }, (_, i) => ({
530+
issueId: `issue-${i}`,
531+
resultDataId: i,
532+
filePath: `file-${i}.ts`,
533+
fileId: i,
534+
patternInfo: { id: "p1", category: "Style", severityLevel: "Warning", level: "Warning" },
535+
toolInfo: { uuid: "t1", name: "Tool" },
536+
lineNumber: 1,
537+
message: `Issue ${i}`,
538+
language: "TypeScript",
539+
lineText: "x",
540+
falsePositiveThreshold: 0.5,
541+
}));
542+
const page2Issues = Array.from({ length: 50 }, (_, i) => ({
543+
issueId: `issue-${100 + i}`,
544+
resultDataId: 100 + i,
545+
filePath: `file-${100 + i}.ts`,
546+
fileId: 100 + i,
547+
patternInfo: { id: "p1", category: "Style", severityLevel: "Warning", level: "Warning" },
548+
toolInfo: { uuid: "t1", name: "Tool" },
549+
lineNumber: 1,
550+
message: `Issue ${100 + i}`,
551+
language: "TypeScript",
552+
lineText: "x",
553+
falsePositiveThreshold: 0.5,
554+
}));
555+
556+
vi.mocked(AnalysisService.searchRepositoryIssues)
557+
.mockResolvedValueOnce({
558+
data: page1Issues,
559+
pagination: { cursor: "cursor-2", limit: 100, total: 250 },
560+
} as any)
561+
.mockResolvedValueOnce({
562+
data: page2Issues,
563+
pagination: { cursor: undefined, limit: 100, total: 250 },
564+
} as any);
565+
566+
const program = createProgram();
567+
await program.parseAsync([
568+
"node",
569+
"test",
570+
"issues",
571+
"gh",
572+
"test-org",
573+
"test-repo",
574+
"--limit",
575+
"150",
576+
]);
577+
578+
expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledTimes(2);
579+
// First call: no cursor
580+
expect(AnalysisService.searchRepositoryIssues).toHaveBeenNthCalledWith(
581+
1, "gh", "test-org", "test-repo", undefined, 100, {},
582+
);
583+
// Second call: with cursor from first response
584+
expect(AnalysisService.searchRepositoryIssues).toHaveBeenNthCalledWith(
585+
2, "gh", "test-org", "test-repo", "cursor-2", 100, {},
586+
);
587+
588+
const output = getAllOutput();
589+
expect(output).toContain("Issues — Found 250 issues");
590+
});
591+
592+
it("should cap limit at 1000", async () => {
593+
vi.mocked(AnalysisService.searchRepositoryIssues).mockResolvedValue({
594+
data: [],
595+
} as any);
596+
597+
const program = createProgram();
598+
await program.parseAsync([
599+
"node",
600+
"test",
601+
"issues",
602+
"gh",
603+
"test-org",
604+
"test-repo",
605+
"--limit",
606+
"5000",
607+
]);
608+
609+
// Should use pageSize 100 (min of 1000, 100)
610+
expect(AnalysisService.searchRepositoryIssues).toHaveBeenCalledWith(
611+
"gh", "test-org", "test-repo", undefined, 100, {},
612+
);
613+
});
614+
501615
it("should fail when CODACY_API_TOKEN is not set", async () => {
502616
delete process.env.CODACY_API_TOKEN;
503617

0 commit comments

Comments
 (0)