/**
* @fileoverview Tests for security client
* This file adds coverage for the previously untested security-client.ts
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SecurityClient } from '../../client/security-client.js';
import type { SecurityClientTestable, MockDeepSourceClient } from '../test-types.js';
import { TestableSecurityClient } from '../utils/test-utils.js';
interface MockBaseClient {
findProjectByKey: ReturnType<typeof vi.fn>;
executeGraphQL: ReturnType<typeof vi.fn>;
createEmptyPaginatedResponse: ReturnType<typeof vi.fn>;
normalizePaginationParams: ReturnType<typeof vi.fn>;
logger: {
info: ReturnType<typeof vi.fn>;
error: ReturnType<typeof vi.fn>;
debug: ReturnType<typeof vi.fn>;
warn: ReturnType<typeof vi.fn>;
};
}
describe('SecurityClient', () => {
let securityClient: SecurityClient;
let mockBaseClient: MockDeepSourceClient;
let mockedClient: MockBaseClient;
beforeEach(() => {
securityClient = new SecurityClient('test-api-key');
mockBaseClient = securityClient as unknown as MockDeepSourceClient;
// Mock the methods we need
mockedClient = mockBaseClient as unknown as MockBaseClient;
mockedClient.findProjectByKey = vi.fn();
mockedClient.executeGraphQL = vi.fn();
mockedClient.createEmptyPaginatedResponse = vi.fn();
mockedClient.normalizePaginationParams = vi.fn();
mockedClient.logger = {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
warn: vi.fn(),
};
});
describe('getComplianceReport', () => {
it('should fetch compliance report successfully', async () => {
const mockProject = {
repository: {
login: 'test-org',
name: 'test-repo',
provider: 'github',
},
};
const mockResponse = {
data: {
repository: {
reports: {
owaspTop10: {
status: 'FAILING',
categories: [
{
name: 'Injection',
status: 'FAILING',
criticalCount: 2,
majorCount: 1,
minorCount: 0,
total: 3,
},
{
name: 'Broken Authentication',
status: 'PASSING',
criticalCount: 0,
majorCount: 0,
minorCount: 0,
total: 0,
},
],
},
},
},
},
};
mockedClient.findProjectByKey = vi.fn().mockResolvedValue(mockProject);
mockedClient.executeGraphQL = vi.fn().mockResolvedValue(mockResponse as any); // skipcq: JS-0323
const result = await securityClient.getComplianceReport('test-project', 'OWASP_TOP_10');
expect(mockedClient.findProjectByKey).toHaveBeenCalledWith('test-project');
expect(mockedClient.executeGraphQL).toHaveBeenCalledWith(
expect.stringContaining('query getComplianceReports'),
expect.objectContaining({
login: 'test-org',
name: 'test-repo',
provider: 'github',
})
);
expect(result).not.toBeNull();
if (result) {
expect(result.reportType).toBe('OWASP_TOP_10');
expect(result.status).toBe('FAILING');
expect(result.categories).toHaveLength(2);
expect(result.categories[0].name).toBe('Injection');
}
});
it('should return null when project not found', async () => {
mockedClient.findProjectByKey = vi.fn().mockResolvedValue(null);
const result = await securityClient.getComplianceReport(
'nonexistent-project',
'OWASP_TOP_10'
);
expect(result).toBeNull();
});
it('should handle GraphQL errors', async () => {
const mockProject = {
repository: {
login: 'test-org',
name: 'test-repo',
provider: 'github',
},
};
mockedClient.findProjectByKey = vi.fn().mockResolvedValue(mockProject);
vi.spyOn(mockedClient, 'executeGraphQL').mockRejectedValue(new Error('GraphQL error'));
await expect(
securityClient.getComplianceReport('test-project', 'OWASP_TOP_10')
).rejects.toThrow('GraphQL error');
});
it('should return null when no data in response', async () => {
const mockProject = {
repository: {
login: 'test-org',
name: 'test-repo',
provider: 'github',
},
};
const mockResponse = {
data: null,
};
mockedClient.findProjectByKey = vi.fn().mockResolvedValue(mockProject);
mockedClient.executeGraphQL = vi.fn().mockResolvedValue(mockResponse as any); // skipcq: JS-0323
const result = await securityClient.getComplianceReport('test-project', 'OWASP_TOP_10');
expect(result).toBeNull();
});
it('should handle missing compliance report in response', async () => {
const mockProject = {
repository: {
login: 'test-org',
name: 'test-repo',
provider: 'github',
},
};
const mockResponse = {
data: {
repository: {
complianceReport: null,
},
},
};
mockedClient.findProjectByKey = vi.fn().mockResolvedValue(mockProject);
mockedClient.executeGraphQL = vi.fn().mockResolvedValue(mockResponse as any); // skipcq: JS-0323
const result = await securityClient.getComplianceReport('test-project', 'OWASP_TOP_10');
expect(result).toBeNull();
});
});
describe('getDependencyVulnerabilities', () => {
it('should fetch dependency vulnerabilities successfully', async () => {
const mockProject = {
repository: {
login: 'test-org',
name: 'test-repo',
provider: 'github',
},
};
const mockResponse = {
data: {
repository: {
dependencyVulnerabilities: {
edges: [
{
node: {
id: 'vuln-1',
package: {
id: 'pkg-1',
ecosystem: 'npm',
name: 'lodash',
},
packageVersion: {
id: 'ver-1',
version: '4.17.20',
},
vulnerability: {
id: 'cve-1',
identifier: 'CVE-2023-1234',
summary: 'Prototype pollution vulnerability',
severity: 'HIGH',
fixedVersions: ['4.17.21'],
},
},
},
{
node: {
id: 'vuln-2',
package: {
id: 'pkg-2',
ecosystem: 'npm',
name: 'express',
},
packageVersion: {
id: 'ver-2',
version: '4.16.0',
},
vulnerability: {
id: 'cve-2',
identifier: 'CVE-2023-5678',
summary: 'Security bypass vulnerability',
severity: 'MEDIUM',
fixedVersions: ['4.18.0'],
},
},
},
],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
},
},
};
mockedClient.findProjectByKey = vi.fn().mockResolvedValue(mockProject);
mockedClient.normalizePaginationParams = vi.fn().mockReturnValue({
first: 20,
after: null,
});
mockedClient.executeGraphQL = vi.fn().mockResolvedValue(mockResponse as any); // skipcq: JS-0323
const result = await securityClient.getDependencyVulnerabilities('test-project');
expect(mockedClient.findProjectByKey).toHaveBeenCalledWith('test-project');
expect(mockedClient.executeGraphQL).toHaveBeenCalledWith(
expect.stringContaining('query getDependencyVulnerabilities'),
expect.objectContaining({
login: 'test-org',
name: 'test-repo',
provider: 'github',
})
);
expect(result.items).toHaveLength(2);
expect(result.items[0].vulnerability.identifier).toBe('CVE-2023-1234');
expect(result.items[0].package.name).toBe('lodash');
expect(result.items[1].vulnerability.severity).toBe('MEDIUM');
expect(result.pageInfo.hasNextPage).toBe(false);
});
it('should return empty response when project not found', async () => {
mockedClient.findProjectByKey = vi.fn().mockResolvedValue(null);
mockedClient.createEmptyPaginatedResponse = vi.fn().mockReturnValue({
items: [],
pageInfo: { hasNextPage: false, hasPreviousPage: false },
totalCount: 0,
});
const result = await securityClient.getDependencyVulnerabilities('nonexistent-project');
expect(result.items).toHaveLength(0);
expect(result.totalCount).toBe(0);
});
it('should handle GraphQL errors', async () => {
const mockProject = {
repository: {
login: 'test-org',
name: 'test-repo',
provider: 'github',
},
};
mockedClient.findProjectByKey = vi.fn().mockResolvedValue(mockProject);
mockedClient.normalizePaginationParams = vi.fn().mockReturnValue({
first: 20,
});
vi.spyOn(mockedClient, 'executeGraphQL').mockRejectedValue(new Error('GraphQL error'));
await expect(securityClient.getDependencyVulnerabilities('test-project')).rejects.toThrow(
'GraphQL error'
);
});
it('should handle pagination parameters', async () => {
const mockProject = {
repository: {
login: 'test-org',
name: 'test-repo',
provider: 'github',
},
};
const mockResponse = {
data: {
repository: {
vulnerabilities: {
edges: [],
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
},
},
},
};
mockedClient.findProjectByKey = vi.fn().mockResolvedValue(mockProject);
mockedClient.normalizePaginationParams = vi.fn().mockReturnValue({
first: 10,
after: 'cursor123',
});
mockedClient.executeGraphQL = vi.fn().mockResolvedValue(mockResponse as any); // skipcq: JS-0323
await securityClient.getDependencyVulnerabilities('test-project', {
first: 10,
after: 'cursor123',
});
// The normalizePaginationParams is now a static method and its behavior
// is tested separately in base-client tests
});
});
describe('buildComplianceReportQuery', () => {
it('should build correct GraphQL query', () => {
const query = TestableSecurityClient.testBuildComplianceReportQuery();
expect(query).toContain('query getComplianceReports');
expect(query).toContain('$login: String!');
expect(query).toContain('$name: String!');
expect(query).toContain('$provider: VCSProvider!');
expect(query).toContain('reports');
});
});
describe('buildVulnerabilitiesQuery', () => {
it('should build correct GraphQL query', () => {
// Add a test method for buildVulnerabilitiesQuery to TestableSecurityClient
const query = TestableSecurityClient.testBuildVulnerabilitiesQuery();
expect(query).toContain('query getDependencyVulnerabilities');
expect(query).toContain('$login: String!');
expect(query).toContain('$name: String!');
expect(query).toContain('$provider: VCSProvider!');
expect(query).toContain('dependencyVulnerabilities');
});
});
describe('extractComplianceReportFromResponse', () => {
it('should extract compliance report from GraphQL response', () => {
const mockResponseData = {
repository: {
reports: {
owaspTop10: {
status: 'FAILING',
categories: [
{
name: 'Injection',
status: 'FAILING',
criticalCount: 2,
majorCount: 1,
minorCount: 0,
total: 3,
},
],
},
},
},
};
const report = (
securityClient as unknown as SecurityClientTestable
).extractComplianceReportFromResponse(mockResponseData, 'OWASP_TOP_10', 'owaspTop10');
expect(report).not.toBeNull();
expect(report.reportType).toBe('OWASP_TOP_10');
expect(report.status).toBe('FAILING');
expect(report.categories).toHaveLength(1);
});
it('should return null when compliance report is missing', () => {
const mockResponseData = {
repository: {
reports: {
owaspTop10: null,
},
},
};
const report = (
securityClient as unknown as SecurityClientTestable
).extractComplianceReportFromResponse(mockResponseData, 'OWASP_TOP_10', 'owaspTop10');
expect(report).toBeNull();
});
it('should handle missing repository in response', () => {
const mockResponseData = {};
const report = (
securityClient as unknown as SecurityClientTestable
).extractComplianceReportFromResponse(mockResponseData, 'OWASP_TOP_10', 'owaspTop10');
expect(report).toBeNull();
});
});
describe('extractVulnerabilitiesFromResponse', () => {
it('should extract vulnerabilities from GraphQL response', () => {
const mockResponseData = {
repository: {
dependencyVulnerabilities: {
edges: [
{
node: {
id: 'vuln-1',
package: {
id: 'pkg-1',
ecosystem: 'npm',
name: 'lodash',
},
packageVersion: {
id: 'ver-1',
version: '4.17.20',
},
vulnerability: {
id: 'cve-1',
identifier: 'CVE-2023-1234',
summary: 'Prototype pollution vulnerability',
severity: 'HIGH',
fixedVersions: ['4.17.21'],
},
},
},
],
},
},
};
const vulnerabilities = (
securityClient as unknown as SecurityClientTestable
).extractVulnerabilitiesFromResponse(mockResponseData);
expect(vulnerabilities).toHaveLength(1);
expect(vulnerabilities[0].vulnerability.identifier).toBe('CVE-2023-1234');
expect(vulnerabilities[0].package.name).toBe('lodash');
});
it('should handle missing vulnerabilities in response', () => {
const mockResponseData = {
repository: {},
};
const vulnerabilities = (
securityClient as unknown as SecurityClientTestable
).extractVulnerabilitiesFromResponse(mockResponseData);
expect(vulnerabilities).toHaveLength(0);
});
});
});