Skip to content
Merged
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
48 changes: 17 additions & 31 deletions tests/integration/specs/api/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,28 +46,27 @@ describe('api', () => {
{ timeout: 15000 },
);

it(
'exits with code 2 for an invalid HTTP method',
async () => {
harness.withAuth('http://localhost:19999', 'fake-token');

const result = await harness.run('api trace /api/system/status');

expect(result.exitCode).toBe(2);
expect(result.stdout + result.stderr).toContain("Invalid HTTP method 'trace'");
},
{ timeout: 15000 },
);

it(
'exits with code 2 when endpoint does not start with /',
async () => {
it.each([
['for an invalid HTTP method', 'api trace /api/system/status', "Invalid HTTP method 'trace'"],
[
'when endpoint does not start with /',
'api get api/system/status',
"Endpoint must start with '/'",
],
[
'when --data is not valid JSON',
'api post /api/system/status --data not-json',
'--data must be valid JSON',
],
])(
'exits with code 2 %s',
async (_, command, expectedError) => {
harness.withAuth('http://localhost:19999', 'fake-token');

const result = await harness.run('api get api/system/status');
const result = await harness.run(command);

expect(result.exitCode).toBe(2);
expect(result.stdout + result.stderr).toContain("Endpoint must start with '/'");
expect(result.stdout + result.stderr).toContain(expectedError);
},
{ timeout: 15000 },
);
Expand Down Expand Up @@ -98,19 +97,6 @@ describe('api', () => {
{ timeout: 15000 },
);

it(
'exits with code 2 when --data is not valid JSON',
async () => {
harness.withAuth('http://localhost:19999', 'fake-token');

const result = await harness.run('api post /api/system/status --data not-json');

expect(result.exitCode).toBe(2);
expect(result.stdout + result.stderr).toContain('--data must be valid JSON');
},
{ timeout: 15000 },
);

it(
'returns response body from a successful GET request',
async () => {
Expand Down
53 changes: 21 additions & 32 deletions tests/integration/specs/list/list-issues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,16 +711,32 @@ describe('list issues — argument validation', () => {
{ timeout: 15000 },
);

it(
'exits with code 2 when --page-size is greater than 500',
async () => {
it.each([
[
'--page-size is greater than 500',
'list issues --project my-project --page-size 501',
'Invalid --page-size',
],
[
'--severities is not a recognised value',
'list issues --project my-project --severities UNKNOWN',
'Invalid severity',
],
[
'--statuses is not a recognised value',
'list issues --project my-project --statuses UNKNOWN',
'Invalid status(es)',
],
])(
'exits with code 2 when %s',
async (_, command, expectedError) => {
// Validation runs inside the handler — auth must pass first
harness.withAuth('http://localhost:19999', 'fake-token');

const result = await harness.run('list issues --project my-project --page-size 501');
const result = await harness.run(command);

expect(result.exitCode).toBe(2);
expect(result.stdout + result.stderr).toContain('Invalid --page-size');
expect(result.stdout + result.stderr).toContain(expectedError);
},
{ timeout: 15000 },
);
Expand All @@ -741,19 +757,6 @@ describe('list issues — argument validation', () => {
{ timeout: 15000 },
);

it(
'exits with code 2 when --severities is not a recognised value',
async () => {
harness.withAuth('http://localhost:19999', 'fake-token');

const result = await harness.run('list issues --project my-project --severities UNKNOWN');

expect(result.exitCode).toBe(2);
expect(result.stdout + result.stderr).toContain('Invalid severity');
},
{ timeout: 15000 },
);

it(
'sends impactSeverities param on MQR server',
async () => {
Expand Down Expand Up @@ -865,20 +868,6 @@ describe('list issues — argument validation', () => {
{ timeout: 15000 },
);

it(
'exits with code 2 when --statuses is not a recognised value',
async () => {
// Validation runs inside the handler — auth must pass first
harness.withAuth('http://localhost:19999', 'fake-token');

const result = await harness.run('list issues --project my-project --statuses UNKNOWN');

expect(result.exitCode).toBe(2);
expect(result.stdout + result.stderr).toContain('Invalid status(es)');
},
{ timeout: 15000 },
);

it('passes multiple statuses to API when --statuses is provided with multiple values', async () => {
const server = await harness
.newFakeServer()
Expand Down
71 changes: 11 additions & 60 deletions tests/integration/specs/run/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,56 +156,25 @@ describe('run mcp', () => {
{ timeout: 15000 },
);

it(
'passes SONARQUBE_DEBUG_ENABLED=true to docker when --debug is set',
async () => {
const server = await harness.newFakeServer().withAuthToken('test-token').start();
harness.withAuth(server.baseUrl(), 'test-token');
const fakeBinDir = setupFakeDocker();

const result = await harness.run('run mcp --debug', {
extraEnv: { PATH: `${fakeBinDir}${PATH_SEP}${process.env.PATH ?? ''}` },
});

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('-e SONARQUBE_DEBUG_ENABLED');
expect(result.stdout).toContain('ENV_DEBUG_ENABLED=true');
},
{ timeout: 15000 },
);

it(
'passes SONARQUBE_READ_ONLY=true to docker when --read-only is set',
async () => {
const server = await harness.newFakeServer().withAuthToken('test-token').start();
harness.withAuth(server.baseUrl(), 'test-token');
const fakeBinDir = setupFakeDocker();

const result = await harness.run('run mcp --read-only', {
extraEnv: { PATH: `${fakeBinDir}${PATH_SEP}${process.env.PATH ?? ''}` },
});

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('-e SONARQUBE_READ_ONLY');
expect(result.stdout).toContain('ENV_READ_ONLY=true');
},
{ timeout: 15000 },
);

it(
'passes SONARQUBE_TOOLSETS to docker when --toolsets is set',
async () => {
it.each([
['--debug', '-e SONARQUBE_DEBUG_ENABLED', 'ENV_DEBUG_ENABLED=true'],
['--read-only', '-e SONARQUBE_READ_ONLY', 'ENV_READ_ONLY=true'],
['--toolsets issues,rules', '-e SONARQUBE_TOOLSETS', 'ENV_TOOLSETS=issues,rules'],
['--toolsets cag,vortex,issues', '-e SONARQUBE_TOOLSETS', 'ENV_TOOLSETS=cag,vortex,issues'],
])(
'passes correct env var to docker for: %s',
async (flag, envFlag, envValue) => {
const server = await harness.newFakeServer().withAuthToken('test-token').start();
harness.withAuth(server.baseUrl(), 'test-token');
const fakeBinDir = setupFakeDocker();

const result = await harness.run('run mcp --toolsets issues,rules', {
const result = await harness.run(`run mcp ${flag}`, {
extraEnv: { PATH: `${fakeBinDir}${PATH_SEP}${process.env.PATH ?? ''}` },
});

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('-e SONARQUBE_TOOLSETS');
expect(result.stdout).toContain('ENV_TOOLSETS=issues,rules');
expect(result.stdout).toContain(envFlag);
expect(result.stdout).toContain(envValue);
},
{ timeout: 15000 },
);
Expand Down Expand Up @@ -235,24 +204,6 @@ describe('run mcp', () => {
{ timeout: 15000 },
);

it(
'passes cag/vortex through when explicitly included in --toolsets',
async () => {
const server = await harness.newFakeServer().withAuthToken('test-token').start();
harness.withAuth(server.baseUrl(), 'test-token');
const fakeBinDir = setupFakeDocker();

const result = await harness.run('run mcp --toolsets cag,vortex,issues', {
extraEnv: { PATH: `${fakeBinDir}${PATH_SEP}${process.env.PATH ?? ''}` },
});

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('-e SONARQUBE_TOOLSETS');
expect(result.stdout).toContain('ENV_TOOLSETS=cag,vortex,issues');
},
{ timeout: 15000 },
);

it(
'propagates non-zero container exit code to CLI exit code',
async () => {
Expand Down
56 changes: 8 additions & 48 deletions tests/unit/commands/integrate/antigravity/health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,26 +64,30 @@ describe('checkAntigravitySecretsHookFile', () => {
expect(checkAntigravitySecretsHookFile(join(tempDir, 'hooks.json'))).toBe('not_configured');
});

it('returns configured when sonar-secrets block and script exist (absolute command path)', () => {
it.each([
['configured', true, 'view_file'],
['not_configured', false, 'view_file'],
['invalid', true, 'edit_file'],
] as const)('returns %s when enabled=%s and matcher=%s', (expected, enabled, matcher) => {
tempDir = mkdtempSync(join(tmpdir(), 'sonar-antigravity-health-'));
const scriptPath = join(tempDir, hookScriptName());
writeFileSync(scriptPath, '#!/bin/bash\n');
writeFileSync(
join(tempDir, 'hooks.json'),
JSON.stringify({
'sonar-secrets': {
enabled: true,
enabled,
PreToolUse: [
{
matcher: 'view_file',
matcher,
hooks: [{ command: formatAntigravityHookCommand(scriptPath) }],
},
],
},
}),
);

expect(checkAntigravitySecretsHookFile(join(tempDir, 'hooks.json'))).toBe('configured');
expect(checkAntigravitySecretsHookFile(join(tempDir, 'hooks.json'))).toBe(expected);
});

it('returns configured when the hook command path is relative to hooks.json', () => {
Expand Down Expand Up @@ -149,54 +153,10 @@ describe('checkAntigravitySecretsHookFile', () => {
expect(checkAntigravitySecretsHookFile(join(tempDir, 'hooks.json'))).toBe('invalid');
});

it('returns not_configured when the sonar-secrets block is disabled', () => {
tempDir = mkdtempSync(join(tmpdir(), 'sonar-antigravity-health-'));
const scriptPath = join(tempDir, hookScriptName());
writeFileSync(scriptPath, '#!/bin/bash\n');
writeFileSync(
join(tempDir, 'hooks.json'),
JSON.stringify({
'sonar-secrets': {
enabled: false,
PreToolUse: [
{
matcher: 'view_file',
hooks: [{ command: formatAntigravityHookCommand(scriptPath) }],
},
],
},
}),
);

expect(checkAntigravitySecretsHookFile(join(tempDir, 'hooks.json'))).toBe('not_configured');
});

it('returns not_configured when the sonar-secrets block is absent', () => {
tempDir = mkdtempSync(join(tmpdir(), 'sonar-antigravity-health-'));
writeFileSync(join(tempDir, 'hooks.json'), JSON.stringify({ other: { enabled: true } }));

expect(checkAntigravitySecretsHookFile(join(tempDir, 'hooks.json'))).toBe('not_configured');
});

it('returns invalid when the PreToolUse matcher is not view_file', () => {
tempDir = mkdtempSync(join(tmpdir(), 'sonar-antigravity-health-'));
const scriptPath = join(tempDir, hookScriptName());
writeFileSync(scriptPath, '#!/bin/bash\n');
writeFileSync(
join(tempDir, 'hooks.json'),
JSON.stringify({
'sonar-secrets': {
enabled: true,
PreToolUse: [
{
matcher: 'edit_file',
hooks: [{ command: formatAntigravityHookCommand(scriptPath) }],
},
],
},
}),
);

expect(checkAntigravitySecretsHookFile(join(tempDir, 'hooks.json'))).toBe('invalid');
});
});
42 changes: 12 additions & 30 deletions tests/unit/core/host/connectivity/network-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,30 +347,21 @@ describe('buildFetchNetworkOptions', () => {
});

describe('noProxy bypass', () => {
it('skips proxy when hostname matches noProxy exactly', () => {
it.each([
[
'hostname matches exactly',
'sonar.internal.corp.com',
'https://sonar.internal.corp.com/api',
],
['hostname matches suffix', 'corp.com', 'https://sonar.corp.com/api'],
['noProxy is wildcard *', '*', 'https://sonar.example.com/api'],
['* in comma-separated list', 'localhost,*', 'https://anything.example.com/api'],
])('skips proxy when %s', (_, noProxy, url) => {
const config = makeConfig({
SONAR_HTTPS_PROXY_URL: 'https://proxy:8080',
SONAR_NO_PROXY: 'sonar.internal.corp.com',
SONAR_NO_PROXY: noProxy,
});
const opts = buildFetchNetworkOptions('https://sonar.internal.corp.com/api', config);
expect(opts.proxy).toBeUndefined();
});

it('skips proxy when hostname matches noProxy suffix', () => {
const config = makeConfig({
SONAR_HTTPS_PROXY_URL: 'https://proxy:8080',
SONAR_NO_PROXY: 'corp.com',
});
const opts = buildFetchNetworkOptions('https://sonar.corp.com/api', config);
expect(opts.proxy).toBeUndefined();
});

it('skips proxy when noProxy is wildcard *', () => {
const config = makeConfig({
SONAR_HTTPS_PROXY_URL: 'https://proxy:8080',
SONAR_NO_PROXY: '*',
});
const opts = buildFetchNetworkOptions('https://sonar.example.com/api', config);
const opts = buildFetchNetworkOptions(url, config);
expect(opts.proxy).toBeUndefined();
});

Expand Down Expand Up @@ -421,15 +412,6 @@ describe('buildFetchNetworkOptions', () => {
);
});

it('* as one entry in a comma-separated list matches everything', () => {
const config = makeConfig({
SONAR_HTTPS_PROXY_URL: 'https://proxy:8080',
SONAR_NO_PROXY: 'localhost,*',
});
const opts = buildFetchNetworkOptions('https://anything.example.com/api', config);
expect(opts.proxy).toBeUndefined();
});

it('port-specific noProxy entry only bypasses matching port', () => {
const config = makeConfig({
SONAR_HTTPS_PROXY_URL: 'https://proxy:8080',
Expand Down