diff --git a/src/features/search/search-code/feature.spec.int.ts b/src/features/search/search-code/feature.spec.int.ts index c4c440a..410fddb 100644 --- a/src/features/search/search-code/feature.spec.int.ts +++ b/src/features/search/search-code/feature.spec.int.ts @@ -31,46 +31,29 @@ describe('searchCode integration', () => { } const options: SearchCodeOptions = { - searchText: 'function', + searchText: 'class', projectId: projectName, top: 10, }; - try { - // Act - make an actual API call to Azure DevOps - const result = await searchCode(connection, options); - - // Assert on the actual response - expect(result).toBeDefined(); - expect(typeof result.count).toBe('number'); - expect(Array.isArray(result.results)).toBe(true); - - // Check structure of returned items (if any) - if (result.results.length > 0) { - const firstResult = result.results[0]; - expect(firstResult.fileName).toBeDefined(); - expect(firstResult.path).toBeDefined(); - expect(firstResult.project).toBeDefined(); - expect(firstResult.repository).toBeDefined(); - - if (firstResult.project) { - expect(firstResult.project.name).toBe(projectName); - } - } - } catch (error) { - // Skip test if the code search extension is not installed - if ( - error instanceof Error && - (error.message.includes('ms.vss-code-search is not installed') || - error.message.includes('Resource not found') || - error.message.includes('Failed to search code')) - ) { - console.log( - 'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization', - ); - return; - } - throw error; + // Act - make an actual API call to Azure DevOps + const result = await searchCode(connection, options); + + // Assert on the actual response + expect(result).toBeDefined(); + expect(typeof result.count).toBe('number'); + expect(Array.isArray(result.results)).toBe(true); + + // Check structure of returned items (if any) + + const firstResult = result.results[0]; + expect(firstResult.fileName).toBeDefined(); + expect(firstResult.path).toBeDefined(); + expect(firstResult.project).toBeDefined(); + expect(firstResult.repository).toBeDefined(); + + if (firstResult.project) { + expect(firstResult.project.name).toBe(projectName); } }); @@ -89,41 +72,22 @@ describe('searchCode integration', () => { } const options: SearchCodeOptions = { - searchText: 'function', + searchText: 'class', projectId: projectName, top: 5, includeContent: true, }; - try { - // Act - make an actual API call to Azure DevOps - const result = await searchCode(connection, options); - - // Assert on the actual response - expect(result).toBeDefined(); - - // Check if content is included (if any results) - if (result.results.length > 0) { - // At least some results should have content - // Note: Some files might fail to fetch content, so we don't expect all to have it - const hasContent = result.results.some((r) => r.content !== undefined); - expect(hasContent).toBe(true); - } - } catch (error) { - // Skip test if the code search extension is not installed - if ( - error instanceof Error && - (error.message.includes('ms.vss-code-search is not installed') || - error.message.includes('Resource not found') || - error.message.includes('Failed to search code')) - ) { - console.log( - 'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization', - ); - return; - } - throw error; - } + // Act - make an actual API call to Azure DevOps + const result = await searchCode(connection, options); + + // Assert on the actual response + expect(result).toBeDefined(); + + // At least some results should have content + // Note: Some files might fail to fetch content, so we don't expect all to have it + const hasContent = result.results.some((r) => r.content !== undefined); + expect(hasContent).toBe(true); }); test('should filter results when filters are provided', async () => { @@ -140,62 +104,37 @@ describe('searchCode integration', () => { ); } - try { - // First get some results to find a repository name - const initialOptions: SearchCodeOptions = { - searchText: 'function', - projectId: projectName, - top: 1, - }; - - const initialResult = await searchCode(connection, initialOptions); - - // Skip if no results found - if (initialResult.results.length === 0) { - console.log('Skipping filter test: No initial results found'); - return; - } - - // Use the repository from the first result for filtering - const repoName = initialResult.results[0].repository.name; - - const filteredOptions: SearchCodeOptions = { - searchText: 'function', - projectId: projectName, - filters: { - Repository: [repoName], - }, - top: 5, - }; - - // Act - make an actual API call to Azure DevOps with filters - const result = await searchCode(connection, filteredOptions); - - // Assert on the actual response - expect(result).toBeDefined(); - - // All results should be from the specified repository - if (result.results.length > 0) { - const allFromRepo = result.results.every( - (r) => r.repository.name === repoName, - ); - expect(allFromRepo).toBe(true); - } - } catch (error) { - // Skip test if the code search extension is not installed - if ( - error instanceof Error && - (error.message.includes('ms.vss-code-search is not installed') || - error.message.includes('Resource not found') || - error.message.includes('Failed to search code')) - ) { - console.log( - 'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization', - ); - return; - } - throw error; - } + // First get some results to find a repository name + const initialOptions: SearchCodeOptions = { + searchText: 'class', + projectId: projectName, + top: 1, + }; + + const initialResult = await searchCode(connection, initialOptions); + + // Use the repository from the first result for filtering + const repoName = initialResult.results[0].repository.name; + + const filteredOptions: SearchCodeOptions = { + searchText: 'class', + projectId: projectName, + filters: { + Repository: [repoName], + }, + top: 5, + }; + + // Act - make an actual API call to Azure DevOps with filters + const result = await searchCode(connection, filteredOptions); + + // Assert on the actual response + expect(result).toBeDefined(); + + const allFromRepo = result.results.every( + (r) => r.repository.name === repoName, + ); + expect(allFromRepo).toBe(true); }); test('should handle pagination', async () => { @@ -212,65 +151,39 @@ describe('searchCode integration', () => { ); } - try { - // Get first page - const firstPageOptions: SearchCodeOptions = { - searchText: 'function', - projectId: projectName, - top: 2, - skip: 0, - }; - - const firstPageResult = await searchCode(connection, firstPageOptions); - - // Skip if not enough results for pagination test - if (firstPageResult.count <= 2) { - console.log('Skipping pagination test: Not enough results'); - return; - } - - // Get second page - const secondPageOptions: SearchCodeOptions = { - searchText: 'function', - projectId: projectName, - top: 2, - skip: 2, - }; - - const secondPageResult = await searchCode(connection, secondPageOptions); - - // Assert on pagination - expect(secondPageResult).toBeDefined(); - expect(secondPageResult.results.length).toBeGreaterThan(0); - - // First and second page should have different results - if ( - firstPageResult.results.length > 0 && - secondPageResult.results.length > 0 - ) { - const firstPagePaths = firstPageResult.results.map((r) => r.path); - const secondPagePaths = secondPageResult.results.map((r) => r.path); - - // Check if there's any overlap between pages - const hasOverlap = firstPagePaths.some((path) => - secondPagePaths.includes(path), - ); - expect(hasOverlap).toBe(false); - } - } catch (error) { - // Skip test if the code search extension is not installed - if ( - error instanceof Error && - (error.message.includes('ms.vss-code-search is not installed') || - error.message.includes('Resource not found') || - error.message.includes('Failed to search code')) - ) { - console.log( - 'Skipping test: Code Search extension is not installed or not available in this Azure DevOps organization', - ); - return; - } - throw error; - } + // Get first page + const firstPageOptions: SearchCodeOptions = { + searchText: 'class', + projectId: projectName, + top: 2, + skip: 0, + }; + + const firstPageResult = await searchCode(connection, firstPageOptions); + + // Get second page + const secondPageOptions: SearchCodeOptions = { + searchText: 'class', + projectId: projectName, + top: 2, + skip: 2, + }; + + const secondPageResult = await searchCode(connection, secondPageOptions); + + // Assert on pagination + expect(secondPageResult).toBeDefined(); + expect(secondPageResult.results.length).toBeGreaterThan(0); + + // First and second page should have different results + + const firstPagePaths = firstPageResult.results.map((r) => r.path); + const secondPagePaths = secondPageResult.results.map((r) => r.path); + + // Check if there's any overlap between pages + const hasOverlap = firstPagePaths.some((path) => + secondPagePaths.includes(path), + ); + expect(hasOverlap).toBe(false); }); }); diff --git a/src/features/search/search-code/feature.ts b/src/features/search/search-code/feature.ts index fd75b55..6ea021c 100644 --- a/src/features/search/search-code/feature.ts +++ b/src/features/search/search-code/feature.ts @@ -60,8 +60,23 @@ export async function searchCode( }, ); + // Check if the response is valid + if (!searchResponse.data) { + throw new AzureDevOpsError('Search API returned an empty response'); + } + const results = searchResponse.data; + // Ensure results has required properties + if (typeof results.count !== 'number') { + results.count = 0; + } + + // Ensure results.results is defined before accessing it + if (!results.results) { + results.results = []; + } + // If includeContent is true, fetch the content for each result if (options.includeContent && results.results.length > 0) { await enrichResultsWithContent(connection, results.results); @@ -114,17 +129,40 @@ function extractOrgAndProject( connection: WebApi, projectId: string, ): { organization: string; project: string } { + if (!connection || !connection.serverUrl) { + throw new AzureDevOpsValidationError('Invalid connection object'); + } + // Extract organization from the connection URL const url = connection.serverUrl; - const match = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/); - const organization = match ? match[1] : ''; + + // Handle different Azure DevOps URL formats + let organization = ''; + + // Try dev.azure.com format + const azureMatch = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)/); + if (azureMatch && azureMatch[1]) { + organization = azureMatch[1]; + } + + // Try visualstudio.com format if dev.azure.com format didn't match + if (!organization) { + const vstsMatch = url.match(/https?:\/\/([^.]+)\.visualstudio\.com/); + if (vstsMatch && vstsMatch[1]) { + organization = vstsMatch[1]; + } + } if (!organization) { throw new AzureDevOpsValidationError( - 'Could not extract organization from connection URL', + `Could not extract organization from connection URL: ${url}`, ); } + if (!projectId) { + throw new AzureDevOpsValidationError('Project ID is required'); + } + return { organization, project: projectId, @@ -176,6 +214,11 @@ async function enrichResultsWithContent( connection: WebApi, results: CodeSearchResult[], ): Promise { + // If results is undefined or empty, return early + if (!results || results.length === 0) { + return; + } + try { const gitApi = await connection.getGitApi(); @@ -183,12 +226,24 @@ async function enrichResultsWithContent( await Promise.all( results.map(async (result) => { try { + // Skip if the result doesn't have necessary properties + if (!result.repository?.id || !result.path || !result.project?.name) { + console.warn( + 'Skipping result with missing properties:', + result.path || 'unknown path', + ); + return; + } + + // Check if versions exists and has items before accessing + const changeId = result.versions?.[0]?.changeId; + // Get the file content using the Git API const content = await gitApi.getItemContent( result.repository.id, result.path, result.project.name, - result.versions[0]?.changeId, + changeId, ); // Convert the buffer to a string and store it in the result