Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,43 @@ export class ApexGuruAuthService {
}
return await this.mintOrgJwt();
}

/**
* Perform HTTP request using fetch with Authorization Bearer token
* @param method - HTTP method (GET, POST, etc.)
* @param path - API path (e.g., '/services/data/v64.0/apexguru/validate')
* @param body - Optional request body
* @returns Promise<T> - Parsed JSON response
* @throws Error if request fails or returns non-200 status
*/
async curlRequest<T>(method: string, path: string, body?: unknown): Promise<T> {
try {
const url = `${this.getInstanceUrl()}${path}`;
const accessToken = this.getAccessToken();

const response = await fetch(url, {
method,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: body ? JSON.stringify(body) : undefined
});

if (!response.ok) {
const errorText = await response.text();
throw new Error(
`HTTP ${response.status} ${response.statusText} at ${path}: ${errorText}`
);
}

return await response.json() as T;
} catch (error) {
if (error instanceof Error && error.message.startsWith('HTTP ')) {
throw error;
}
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Request failed for ${path}: ${errorMessage}`);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@


import { Connection } from '@salesforce/core';
import { LogLevel } from '@salesforce/code-analyzer-engine-api';
import { ApexGuruAuthService } from './ApexGuruAuthService';
import {
Expand Down Expand Up @@ -110,14 +109,10 @@
* Internal validate implementation (without timeout wrapper)
*/
private async performValidate(): Promise<void> {
const connection: Connection = this.authService.getConnection();
const apiVersion = this.authService.getApiVersion();
const url = `/services/data/v${apiVersion}/apexguru/validate`;

const response = await connection.request({
method: 'GET',
url
}) as { status?: string };
const response: { status?: string } = await (this.authService as any).curlRequest('GET', url);

Check failure on line 115 in packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts

View workflow job for this annotation

GitHub Actions / run_tests (macos-latest)

Unexpected any. Specify a different type

Check failure on line 115 in packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts

View workflow job for this annotation

GitHub Actions / run_tests (ubuntu-latest)

Unexpected any. Specify a different type

if (response.status && response.status.toLowerCase() === ApexGuruResponseStatus.SUCCESS) {
return;
Expand Down Expand Up @@ -167,20 +162,14 @@
* Submit Apex class for analysis
*/
private async submitAnalysis(classContent: string): Promise<string> {
const connection: Connection = this.authService.getConnection();
const apiVersion = this.authService.getApiVersion();
const url = `/services/data/v${apiVersion}/apexguru/request`;

const base64Content = Buffer.from(classContent, 'utf-8').toString('base64');
const requestBody = { classContent: base64Content };

try {
const response: ApexGuruInitialResponse = await connection.request({
method: 'POST',
url,
body: JSON.stringify(requestBody),
headers: { 'Content-Type': 'application/json' }
});
const response: ApexGuruInitialResponse = await (this.authService as any).curlRequest('POST', url, requestBody);

Check failure on line 172 in packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts

View workflow job for this annotation

GitHub Actions / run_tests (macos-latest)

Unexpected any. Specify a different type

Check failure on line 172 in packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts

View workflow job for this annotation

GitHub Actions / run_tests (ubuntu-latest)

Unexpected any. Specify a different type

// Normalize status to lowercase
if (response.status) {
Expand All @@ -207,7 +196,6 @@
* Note: Timeout is handled by analyzeApexClass wrapper, not here
*/
private async pollForResults(requestId: string): Promise<{violations: ApexGuruViolation[], scanMetadata?: ApexGuruScanMetadata}> {
const connection: Connection = this.authService.getConnection();
const apiVersion = this.authService.getApiVersion();
const url = requestId === 'pending'
? `/services/data/v${apiVersion}/apexguru/request`
Expand All @@ -234,10 +222,7 @@
this.progressCallback(asymptoticProgress);
}

const response: ApexGuruQueryResponse = await connection.request({
method: 'GET',
url
});
const response: ApexGuruQueryResponse = await (this.authService as any).curlRequest('GET', url);

Check failure on line 225 in packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts

View workflow job for this annotation

GitHub Actions / run_tests (macos-latest)

Unexpected any. Specify a different type

Check failure on line 225 in packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts

View workflow job for this annotation

GitHub Actions / run_tests (ubuntu-latest)

Unexpected any. Specify a different type

// Normalize status
if (response.status) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,84 @@ describe('ApexGuruAuthService', () => {
// - Should mint new JWT if not cached
// - Should return cached JWT if available
});

describe('curlRequest', () => {
beforeEach(async () => {
const mockOrg = {
getConnection: jest.fn().mockReturnValue(mockConnection)
};
(Org.create as jest.Mock).mockResolvedValue(mockOrg);
await authService.initialize({ targetOrg: 'myorg' });
});

it('should perform GET request with correct headers and URL', async () => {
const mockFetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ status: 'success' })
});
global.fetch = mockFetch as any;

const result = await (authService as any).curlRequest('GET', '/services/data/v64.0/apexguru/validate');

expect(mockFetch).toHaveBeenCalledWith(
'https://test.salesforce.com/services/data/v64.0/apexguru/validate',
{
method: 'GET',
headers: {
'Authorization': 'Bearer mock_access_token',
'Content-Type': 'application/json'
},
body: undefined
}
);
expect(result).toEqual({ status: 'success' });
});

it('should perform POST request with body', async () => {
const mockFetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue({ requestId: '12345' })
});
global.fetch = mockFetch as any;

const requestBody = { classContent: 'base64string' };
const result = await (authService as any).curlRequest('POST', '/services/data/v64.0/apexguru/request', requestBody);

expect(mockFetch).toHaveBeenCalledWith(
'https://test.salesforce.com/services/data/v64.0/apexguru/request',
{
method: 'POST',
headers: {
'Authorization': 'Bearer mock_access_token',
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
}
);
expect(result).toEqual({ requestId: '12345' });
});

it('should throw error for non-200 response', async () => {
const mockFetch = jest.fn().mockResolvedValue({
ok: false,
status: 401,
statusText: 'Unauthorized',
text: jest.fn().mockResolvedValue('Invalid token')
});
global.fetch = mockFetch as any;

await expect((authService as any).curlRequest('GET', '/services/data/v64.0/apexguru/validate'))
.rejects
.toThrow('HTTP 401 Unauthorized at /services/data/v64.0/apexguru/validate: Invalid token');
});

it('should handle network errors', async () => {
const mockFetch = jest.fn().mockRejectedValue(new Error('Network failure'));
global.fetch = mockFetch as any;

await expect((authService as any).curlRequest('GET', '/services/data/v64.0/apexguru/validate'))
.rejects
.toThrow('Request failed for /services/data/v64.0/apexguru/validate: Network failure');
});
});
});
Loading
Loading