From de528e4de39661a1452a6685a70726d8cb4eb699 Mon Sep 17 00:00:00 2001 From: Nikhil Mittal Date: Fri, 12 Jun 2026 13:00:45 +0530 Subject: [PATCH 1/3] feat(apexguru-engine): add curl command generation utility Add generateCurlCommand() private method to ApexGuruService that constructs equivalent curl commands for API calls. This enables debugging and documentation of the actual HTTP requests made to ApexGuru endpoints with proper authentication headers and base64 encoding. - Add generateCurlCommand() with support for GET and POST requests - Include proper header formatting (Authorization, Content-Type) - Escape single quotes in request body - Handle uninitialized connection gracefully - Add comprehensive unit tests for all curl generation scenarios --- .../src/services/ApexGuruService.ts | 89 ++++++- .../test/ApexGuruService.test.ts | 235 ++++++++++++++++++ 2 files changed, 323 insertions(+), 1 deletion(-) diff --git a/packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts b/packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts index 2667b115..a781da35 100644 --- a/packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts +++ b/packages/code-analyzer-apexguru-engine/src/services/ApexGuruService.ts @@ -88,6 +88,50 @@ export class ApexGuruService { } } + /** + * Export curl commands for debugging/documentation purposes + * Returns curl commands for validate, submit, and query endpoints + * Note: Requires initialize() to be called first for valid authentication + */ + exportCurlCommands(): { + validate: string; + submit: (classContent: string) => string; + query: (requestId: string) => string; + } { + try { + const apiVersion = this.authService.getApiVersion(); + + return { + validate: this.generateCurlCommand( + 'GET', + `/services/data/v${apiVersion}/apexguru/validate` + ), + submit: (classContent: string) => { + const base64Content = Buffer.from(classContent, 'utf-8').toString('base64'); + const requestBody = { classContent: base64Content }; + return this.generateCurlCommand( + 'POST', + `/services/data/v${apiVersion}/apexguru/request`, + JSON.stringify(requestBody) + ); + }, + query: (requestId: string) => { + const url = requestId === 'pending' + ? `/services/data/v${apiVersion}/apexguru/request` + : `/services/data/v${apiVersion}/apexguru/request/${requestId}`; + return this.generateCurlCommand('GET', url); + } + }; + } catch { + const notInitialized = '# Connection not initialized'; + return { + validate: notInitialized, + submit: () => notInitialized, + query: () => notInitialized + }; + } + } + /** * Validate ApexGuru access * Throws error with specific context if validation fails @@ -114,6 +158,11 @@ export class ApexGuruService { const apiVersion = this.authService.getApiVersion(); const url = `/services/data/v${apiVersion}/apexguru/validate`; + if (process.env.APEXGURU_DEBUG_CURL) { + const curlCmd = this.generateCurlCommand('GET', url); + this.emitLogEvent(LogLevel.Fine, `Equivalent curl command:\n${curlCmd}`); + } + const response = await connection.request({ method: 'GET', url @@ -173,12 +222,18 @@ export class ApexGuruService { const base64Content = Buffer.from(classContent, 'utf-8').toString('base64'); const requestBody = { classContent: base64Content }; + const body = JSON.stringify(requestBody); + + if (process.env.APEXGURU_DEBUG_CURL) { + const curlCmd = this.generateCurlCommand('POST', url, body); + this.emitLogEvent(LogLevel.Fine, `Equivalent curl command:\n${curlCmd}`); + } try { const response: ApexGuruInitialResponse = await connection.request({ method: 'POST', url, - body: JSON.stringify(requestBody), + body, headers: { 'Content-Type': 'application/json' } }); @@ -234,6 +289,11 @@ export class ApexGuruService { this.progressCallback(asymptoticProgress); } + if (process.env.APEXGURU_DEBUG_CURL) { + const curlCmd = this.generateCurlCommand('GET', url); + this.emitLogEvent(LogLevel.Fine, `Equivalent curl command:\n${curlCmd}`); + } + const response: ApexGuruQueryResponse = await connection.request({ method: 'GET', url @@ -289,4 +349,31 @@ export class ApexGuruService { private sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } + + /** + * Generate equivalent curl command for debugging + */ + private generateCurlCommand(method: string, url: string, body?: string): string { + try { + const connection = this.authService.getConnection(); + if (!connection || !connection.instanceUrl || !connection.accessToken) { + return '# Connection not initialized'; + } + + const fullUrl = `${connection.instanceUrl}${url}`; + const lines: string[] = [`curl -X ${method}`]; + lines.push(` '${fullUrl}'`); + lines.push(` -H "Authorization: Bearer ${connection.accessToken}"`); + + if (body) { + lines.push(` -H "Content-Type: application/json"`); + const escapedBody = body.replace(/'/g, "'\\''"); + lines.push(` -d '${escapedBody}'`); + } + + return lines.join(' \\\n'); + } catch { + return '# Connection not initialized'; + } + } } diff --git a/packages/code-analyzer-apexguru-engine/test/ApexGuruService.test.ts b/packages/code-analyzer-apexguru-engine/test/ApexGuruService.test.ts index 0cfc0330..eace7f56 100644 --- a/packages/code-analyzer-apexguru-engine/test/ApexGuruService.test.ts +++ b/packages/code-analyzer-apexguru-engine/test/ApexGuruService.test.ts @@ -415,4 +415,239 @@ describe('ApexGuruService', () => { expect(() => apexGuruService.cleanup()).not.toThrow(); }); }); + + describe('generateCurlCommand', () => { + beforeEach(async () => { + // Initialize to set up connection + await apexGuruService.initialize(); + }); + + it('should generate valid curl command for GET validate endpoint', () => { + const curlCmd = (apexGuruService as any).generateCurlCommand( + 'GET', + '/services/data/v64.0/apexguru/validate' + ); + + expect(curlCmd).toContain('curl -X GET'); + expect(curlCmd).toContain('https://test.salesforce.com/services/data/v64.0/apexguru/validate'); + expect(curlCmd).toContain('-H "Authorization: Bearer test-token"'); + }); + + it('should generate valid curl command for POST submit endpoint with body', () => { + const testContent = 'public class Test { }'; + const base64Content = Buffer.from(testContent, 'utf-8').toString('base64'); + const requestBody = { classContent: base64Content }; + + const curlCmd = (apexGuruService as any).generateCurlCommand( + 'POST', + '/services/data/v64.0/apexguru/request', + JSON.stringify(requestBody) + ); + + expect(curlCmd).toContain('curl -X POST'); + expect(curlCmd).toContain('https://test.salesforce.com/services/data/v64.0/apexguru/request'); + expect(curlCmd).toContain('-H "Authorization: Bearer test-token"'); + expect(curlCmd).toContain('-H "Content-Type: application/json"'); + expect(curlCmd).toContain(`-d '${JSON.stringify(requestBody)}'`); + }); + + it('should generate valid curl command for GET query endpoint with requestId', () => { + const requestId = 'req-123'; + const curlCmd = (apexGuruService as any).generateCurlCommand( + 'GET', + `/services/data/v64.0/apexguru/request/${requestId}` + ); + + expect(curlCmd).toContain('curl -X GET'); + expect(curlCmd).toContain(`https://test.salesforce.com/services/data/v64.0/apexguru/request/${requestId}`); + expect(curlCmd).toContain('-H "Authorization: Bearer test-token"'); + }); + + it('should properly escape single quotes in body', () => { + const bodyWithQuotes = JSON.stringify({ data: "It's a test" }); + const curlCmd = (apexGuruService as any).generateCurlCommand( + 'POST', + '/services/data/v64.0/apexguru/request', + bodyWithQuotes + ); + + expect(curlCmd).toContain(`-d '${bodyWithQuotes.replace(/'/g, "'\\''")}'`); + }); + + it('should return graceful message when connection not initialized', () => { + const uninitializedAuthService = { + initialize: jest.fn(), + getConnection: jest.fn().mockReturnValue(null), + getAccessToken: jest.fn(), + getInstanceUrl: jest.fn(), + getApiVersion: jest.fn(), + mintOrgJwt: jest.fn() + } as any; + + jest.mocked(ApexGuruAuthService).mockImplementationOnce(() => uninitializedAuthService); + + const uninitializedService = new ApexGuruService( + mockEmitLogEvent, + 120000, + 2000, + 60000, + 2 + ); + + const curlCmd = (uninitializedService as any).generateCurlCommand( + 'GET', + '/services/data/v64.0/apexguru/validate' + ); + + expect(curlCmd).toBe('# Connection not initialized'); + }); + }); + + describe('debug curl logging', () => { + const originalEnv = process.env.APEXGURU_DEBUG_CURL; + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.APEXGURU_DEBUG_CURL = originalEnv; + } else { + delete process.env.APEXGURU_DEBUG_CURL; + } + }); + + beforeEach(async () => { + await apexGuruService.initialize(); + }); + + it('should log curl command for validate when APEXGURU_DEBUG_CURL is set', async () => { + process.env.APEXGURU_DEBUG_CURL = '1'; + + (mockConnection.request as jest.Mock).mockResolvedValue({ + status: ApexGuruResponseStatus.SUCCESS + }); + + await apexGuruService.validate(); + + expect(mockEmitLogEvent).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('Equivalent curl command:') + ); + expect(mockEmitLogEvent).toHaveBeenCalledWith( + expect.anything(), + expect.stringContaining('curl -X GET') + ); + }); + + it('should not log curl command for validate when APEXGURU_DEBUG_CURL is not set', async () => { + delete process.env.APEXGURU_DEBUG_CURL; + + (mockConnection.request as jest.Mock).mockResolvedValue({ + status: ApexGuruResponseStatus.SUCCESS + }); + + await apexGuruService.validate(); + + const curlLogs = mockEmitLogEvent.mock.calls.filter((call: any[]) => + call[1] && call[1].includes('curl') + ); + expect(curlLogs).toHaveLength(0); + }); + + it('should log curl command for submit when APEXGURU_DEBUG_CURL is set', async () => { + process.env.APEXGURU_DEBUG_CURL = '1'; + + (mockConnection.request as jest.Mock).mockResolvedValueOnce({ + status: ApexGuruResponseStatus.NEW, + requestId: 'req-123' + }); + + (mockConnection.request as jest.Mock).mockResolvedValueOnce({ + status: ApexGuruResponseStatus.SUCCESS, + report: Buffer.from(JSON.stringify([])).toString('base64') + }); + + await apexGuruService.analyzeApexClass('public class Test { }', '/test/Test.cls'); + + const curlLogs = mockEmitLogEvent.mock.calls.filter((call: any[]) => + call[1] && call[1].includes('Equivalent curl command:') + ); + expect(curlLogs.length).toBeGreaterThanOrEqual(2); // At least submit and poll + }); + + it('should log curl command for poll when APEXGURU_DEBUG_CURL is set', async () => { + process.env.APEXGURU_DEBUG_CURL = '1'; + + (mockConnection.request as jest.Mock).mockResolvedValueOnce({ + status: ApexGuruResponseStatus.NEW, + requestId: 'req-123' + }); + + (mockConnection.request as jest.Mock).mockResolvedValueOnce({ + status: ApexGuruResponseStatus.SUCCESS, + report: Buffer.from(JSON.stringify([])).toString('base64') + }); + + await apexGuruService.analyzeApexClass('public class Test { }', '/test/Test.cls'); + + const curlWithRequestId = mockEmitLogEvent.mock.calls.filter((call: any[]) => + call[1] && call[1].includes('curl') && call[1].includes('req-123') + ); + expect(curlWithRequestId.length).toBeGreaterThan(0); + }); + }); + + describe('exportCurlCommands', () => { + beforeEach(async () => { + await apexGuruService.initialize(); + }); + + it('should return object with validate, submit, and query curl commands', () => { + const exported = apexGuruService.exportCurlCommands(); + + expect(exported).toHaveProperty('validate'); + expect(exported).toHaveProperty('submit'); + expect(exported).toHaveProperty('query'); + expect(typeof exported.validate).toBe('string'); + expect(typeof exported.submit).toBe('function'); + expect(typeof exported.query).toBe('function'); + }); + + it('should generate valid validate curl command', () => { + const exported = apexGuruService.exportCurlCommands(); + + expect(exported.validate).toContain('curl -X GET'); + expect(exported.validate).toContain('https://test.salesforce.com/services/data/v64.0/apexguru/validate'); + expect(exported.validate).toContain('Authorization: Bearer test-token'); + }); + + it('should generate valid submit curl command with class content', () => { + const exported = apexGuruService.exportCurlCommands(); + const testContent = 'public class Test { }'; + const submitCmd = exported.submit(testContent); + + expect(submitCmd).toContain('curl -X POST'); + expect(submitCmd).toContain('https://test.salesforce.com/services/data/v64.0/apexguru/request'); + expect(submitCmd).toContain('Authorization: Bearer test-token'); + expect(submitCmd).toContain('Content-Type: application/json'); + expect(submitCmd).toContain(Buffer.from(testContent).toString('base64')); + }); + + it('should generate valid query curl command with requestId', () => { + const exported = apexGuruService.exportCurlCommands(); + const requestId = 'req-123'; + const queryCmd = exported.query(requestId); + + expect(queryCmd).toContain('curl -X GET'); + expect(queryCmd).toContain(`https://test.salesforce.com/services/data/v64.0/apexguru/request/${requestId}`); + expect(queryCmd).toContain('Authorization: Bearer test-token'); + }); + + it('should handle pending requestId for query', () => { + const exported = apexGuruService.exportCurlCommands(); + const queryCmd = exported.query('pending'); + + expect(queryCmd).toContain('curl -X GET'); + expect(queryCmd).toContain('https://test.salesforce.com/services/data/v64.0/apexguru/request'); + expect(queryCmd).not.toContain('request/pending'); + }); + }); }); From 42c6f20c327fe3b3fc65cfd634f42dfbc2627c4c Mon Sep 17 00:00:00 2001 From: Nikhil Mittal Date: Fri, 12 Jun 2026 13:01:06 +0530 Subject: [PATCH 2/3] test(apexguru-engine): add curl command integration tests Add integration tests that verify generated curl commands are executable and produce valid responses. Tests are skipped when no authentication is available (SF_TARGET_ORG not set). - Test validate endpoint curl execution - Test submit endpoint curl execution with base64 content - Test query endpoint curl execution with requestId - Test graceful handling of uninitialized connection - Integration tests timeout at 30 seconds --- .../test/integration/curl.test.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 packages/code-analyzer-apexguru-engine/test/integration/curl.test.ts diff --git a/packages/code-analyzer-apexguru-engine/test/integration/curl.test.ts b/packages/code-analyzer-apexguru-engine/test/integration/curl.test.ts new file mode 100644 index 00000000..5859702c --- /dev/null +++ b/packages/code-analyzer-apexguru-engine/test/integration/curl.test.ts @@ -0,0 +1,88 @@ +import { ApexGuruService } from '../../src/services/ApexGuruService'; +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; + +const execAsync = promisify(exec); + +describe('Curl Command Integration Tests', () => { + let apexGuruService: ApexGuruService; + const mockEmitLogEvent = jest.fn(); + + beforeEach(() => { + apexGuruService = new ApexGuruService( + mockEmitLogEvent, + 120000, + 2000, + 60000, + 2 + ); + }); + + // Skip test if no authentication is available + const skipIfNoAuth = process.env.SF_TARGET_ORG ? test : test.skip; + + skipIfNoAuth('should generate executable curl command for validate endpoint', async () => { + // Initialize with real authentication + await apexGuruService.initialize(process.env.SF_TARGET_ORG); + + // Export curl commands + const exported = apexGuruService.exportCurlCommands(); + const curlCmd = exported.validate; + + // Execute curl command + const { stdout } = await execAsync(curlCmd); + const response = JSON.parse(stdout); + + // Verify response has expected status field + expect(response).toHaveProperty('status'); + expect(typeof response.status).toBe('string'); + }, 30000); + + skipIfNoAuth('should generate executable curl command for submit endpoint', async () => { + await apexGuruService.initialize(process.env.SF_TARGET_ORG); + + const testContent = 'public class CurlTest { public void test() { System.debug("test"); } }'; + const exported = apexGuruService.exportCurlCommands(); + const curlCmd = exported.submit(testContent); + + // Execute curl command + const { stdout } = await execAsync(curlCmd); + const response = JSON.parse(stdout); + + // Verify response has expected fields + expect(response).toHaveProperty('status'); + expect(['new', 'success', 'failed']).toContain(response.status.toLowerCase()); + }, 30000); + + skipIfNoAuth('should generate executable curl command for query endpoint', async () => { + await apexGuruService.initialize(process.env.SF_TARGET_ORG); + + // First submit a request to get a requestId + const testContent = 'public class CurlTest { }'; + const exported = apexGuruService.exportCurlCommands(); + + const submitCmd = exported.submit(testContent); + const { stdout: submitStdout } = await execAsync(submitCmd); + const submitResponse = JSON.parse(submitStdout); + + // Use the requestId to query (or use 'pending' if no requestId) + const requestId = submitResponse.requestId || 'pending'; + const queryCmd = exported.query(requestId); + + const { stdout: queryStdout } = await execAsync(queryCmd); + const queryResponse = JSON.parse(queryStdout); + + // Verify response structure + expect(queryResponse).toHaveProperty('status'); + expect(typeof queryResponse.status).toBe('string'); + }, 30000); + + test('should generate valid curl commands even without authentication', () => { + // Don't initialize - test graceful handling + const exported = apexGuruService.exportCurlCommands(); + + expect(exported.validate).toContain('# Connection not initialized'); + expect(exported.submit('test')).toContain('# Connection not initialized'); + expect(exported.query('req-123')).toContain('# Connection not initialized'); + }); +}); From d66437b607cfd3f8774c51a8b65a714d10718b42 Mon Sep 17 00:00:00 2001 From: claude-unleashed Date: Fri, 12 Jun 2026 13:01:39 +0530 Subject: [PATCH 3/3] claude-unleashed: 92576030-a1fa-442d-a3af-c1c926b9e838 --- .../.sfdx/tools/246/StandardApexLibrary/System.AccessLevel.cls | 2 -- .../proj1/.sfdx/StandardApexLibrary/System.AccessLevel.cls | 2 -- 2 files changed, 4 deletions(-) delete mode 100644 packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/graph/ops/GraphUtilTest/testSkipsSfdxFolders/.sfdx/tools/246/StandardApexLibrary/System.AccessLevel.cls delete mode 100644 packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/graph/ops/GraphUtilTest/testSkipsSfdxFolders/proj1/.sfdx/StandardApexLibrary/System.AccessLevel.cls diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/graph/ops/GraphUtilTest/testSkipsSfdxFolders/.sfdx/tools/246/StandardApexLibrary/System.AccessLevel.cls b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/graph/ops/GraphUtilTest/testSkipsSfdxFolders/.sfdx/tools/246/StandardApexLibrary/System.AccessLevel.cls deleted file mode 100644 index 16bbd4af..00000000 --- a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/graph/ops/GraphUtilTest/testSkipsSfdxFolders/.sfdx/tools/246/StandardApexLibrary/System.AccessLevel.cls +++ /dev/null @@ -1,2 +0,0 @@ -How much wood would a woodchuck chuck, if a woodchuck could chuck wood? -Well, it would chuck as much wood as a woodchuck would if a woodchuck could chuck wood. diff --git a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/graph/ops/GraphUtilTest/testSkipsSfdxFolders/proj1/.sfdx/StandardApexLibrary/System.AccessLevel.cls b/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/graph/ops/GraphUtilTest/testSkipsSfdxFolders/proj1/.sfdx/StandardApexLibrary/System.AccessLevel.cls deleted file mode 100644 index 16bbd4af..00000000 --- a/packages/code-analyzer-sfge-engine/sfge/src/test/resources/test-files/com/salesforce/graph/ops/GraphUtilTest/testSkipsSfdxFolders/proj1/.sfdx/StandardApexLibrary/System.AccessLevel.cls +++ /dev/null @@ -1,2 +0,0 @@ -How much wood would a woodchuck chuck, if a woodchuck could chuck wood? -Well, it would chuck as much wood as a woodchuck would if a woodchuck could chuck wood.