diff --git a/workspaces/scorecard/.changeset/six-seas-wear.md b/workspaces/scorecard/.changeset/six-seas-wear.md new file mode 100644 index 00000000000..7320a013d07 --- /dev/null +++ b/workspaces/scorecard/.changeset/six-seas-wear.md @@ -0,0 +1,5 @@ +--- +'@red-hat-developer-hub/backstage-plugin-scorecard-backend-module-sonarqube': patch +--- + +Resolve issue for `sonarqube.openIssues` Scorecard SonarQube metric when the project is inaccessible. diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-sonarqube/src/clients/SonarQubeClient.test.ts b/workspaces/scorecard/plugins/scorecard-backend-module-sonarqube/src/clients/SonarQubeClient.test.ts index 2c89dabce38..e5f9be7510e 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-sonarqube/src/clients/SonarQubeClient.test.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-sonarqube/src/clients/SonarQubeClient.test.ts @@ -83,20 +83,88 @@ describe('SonarQubeClient', () => { }); describe('getOpenIssuesCount', () => { - it('returns the total count of open issues', async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - json: async () => ({ total: 42 }), - }); + it('returns the total count of open issues after verifying project access', async () => { + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ component: { key: 'my-project' } }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + total: 42, + paging: { pageIndex: 1, pageSize: 1, total: 42 }, + }), + }); const result = await client.getOpenIssuesCount('my-project'); expect(result).toBe(42); - expect(mockFetch).toHaveBeenCalledWith( + expect(mockFetch).toHaveBeenNthCalledWith( + 1, + 'https://sonarcloud.io/api/components/show?component=my-project', + expect.any(Object), + ); + expect(mockFetch).toHaveBeenNthCalledWith( + 2, 'https://sonarcloud.io/api/issues/search?componentKeys=my-project&statuses=OPEN,CONFIRMED,REOPENED&ps=1', expect.any(Object), ); }); + + it('throws when project access check fails and does not search issues', async () => { + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect(client.getOpenIssuesCount('my-project')).rejects.toThrow( + "SonarQube project 'my-project' is not accessible or the project key is missing or invalid", + ); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + 'https://sonarcloud.io/api/components/show?component=my-project', + expect.any(Object), + ); + }); + + it('propagates API errors from issues search after access check succeeds', async () => { + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ component: { key: 'my-project' } }), + }) + .mockResolvedValueOnce({ + ok: false, + status: 503, + statusText: 'Service Unavailable', + }); + + await expect(client.getOpenIssuesCount('my-project')).rejects.toThrow( + /SonarQube API error: 503 Service Unavailable/, + ); + expect(mockFetch).toHaveBeenCalledTimes(2); + }); + + it('returns 0 when the project is accessible and has no open issues', async () => { + mockFetch + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ component: { key: 'my-project' } }), + }) + .mockResolvedValueOnce({ + ok: true, + json: async () => ({ + total: 0, + paging: { pageIndex: 1, pageSize: 1, total: 0 }, + }), + }); + + const result = await client.getOpenIssuesCount('my-project'); + + expect(result).toBe(0); + }); }); describe('getMeasures', () => { diff --git a/workspaces/scorecard/plugins/scorecard-backend-module-sonarqube/src/clients/SonarQubeClient.ts b/workspaces/scorecard/plugins/scorecard-backend-module-sonarqube/src/clients/SonarQubeClient.ts index 6aac7e92fcb..0cb121bb2bf 100644 --- a/workspaces/scorecard/plugins/scorecard-backend-module-sonarqube/src/clients/SonarQubeClient.ts +++ b/workspaces/scorecard/plugins/scorecard-backend-module-sonarqube/src/clients/SonarQubeClient.ts @@ -107,14 +107,29 @@ export class SonarQubeClient { projectKey: string, instanceName?: string, ): Promise { - this.logger.debug(`Fetching open issues count for project ${projectKey}`); - const data = await this.fetchApi( - `/api/issues/search?componentKeys=${encodeURIComponent( - projectKey, - )}&statuses=OPEN,CONFIRMED,REOPENED&ps=1`, - instanceName, - ); - return data.total; + this.logger.debug(`Fetching open issues count for project ${projectKey}`); + + // Additional check to ensure the project is accessible + try { + await this.fetchApi( + `/api/components/show?component=${encodeURIComponent(projectKey)}`, + instanceName, + ); + } + catch { + throw new Error( + `SonarQube project '${projectKey}' is not accessible or the project key is missing or invalid`, + ); + } + + const data = await this.fetchApi( + `/api/issues/search?componentKeys=${encodeURIComponent( + projectKey, + )}&statuses=OPEN,CONFIRMED,REOPENED&ps=1`, + instanceName, + ); + + return data.paging?.total ?? data.total ?? 0; } async getMeasures(