From 16ccd3920466ab104d9bc0a054d9bd6021390a8b Mon Sep 17 00:00:00 2001 From: Michael Jabbour Date: Wed, 17 Jun 2026 10:17:38 +0200 Subject: [PATCH] CLI-693 Migrate CAG integration eligibility check to use the new entitlement endpoint CAG setup in sonar integrate still queried the legacy cag-org-config endpoint and interpreted eligible/enabled flags when deciding whether to install Context Augmentation. That misses organizations whose access is now represented by the server-side CAG entitlement decision and also leaves the CLI warning tied to enablement-specific wording. The mismatch happens at the CLI/backend boundary: the backend now owns GA entitlement, beta fallback, and temporary policy decisions behind a single allowed flag, while the CLI only needs to know whether setup should proceed. This commit switches SonarQubeClient.hasCagEntitlement to /a3s-analysis/cag-entitlement/{uuid}, maps allowed/not_allowed/check_failed explicitly, and updates the shared Context Augmentation setup warning for denied access to mention eligible SonarQube Cloud plans. The shared helper continues to cover Claude, Copilot, Codex, and Antigravity without per-agent branching. The fake SonarQube server and CAG setup tests now use the new allowed response shape, including an endpoint-failure path. The old internal enabled/not_enabled CAG status names were not kept because they no longer match the endpoint contract; SQAA and SCA status naming remains unchanged. Validation: bun run format; bun test tests/unit/sonarqube/client.test.ts; bun test tests/integration/harness/fake-sonarqube-server.test.ts; bun run pretest:integration; bun run typecheck. The focused Context Augmentation integration spec could not be used locally because this machine has parent .git directories under temp roots, causing the harness to resolve the test project root outside its cwd. --- CLAUDE.md | 2 +- .../integrate/_common/context-augmentation.ts | 6 +- src/sonarqube/client.ts | 10 ++-- tests/e2e/context/cag-integrate.test.ts | 10 ++-- .../harness/fake-sonarqube-server.test.ts | 27 ++------- .../harness/fake-sonarqube-server.ts | 60 ++++++++++++------- .../integrate/context-augmentation.test.ts | 51 ++++++++++++++-- .../integrate/claude/integrate.test.ts | 2 +- tests/unit/sonarqube/client.test.ts | 36 +++++------ 9 files changed, 121 insertions(+), 83 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 07459f8e6..052c27043 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,7 +60,7 @@ All three integrations build the feature with the shared `createSonarSecretsHook `--help`, `-h`, and bare `sonar context` (no action) are forwarded to CAG. -Before installing, `sonar integrate claude|copilot|codex|antigravity` pre-flights the CAG entitlement check: `SonarQubeClient.hasCagEntitlement(orgKey)` resolves the org UUID via `/organizations/organizations` then calls `GET /a3s-analysis/cag-org-config/{uuid}` (SonarQube Cloud only). If `eligible && enabled` is false, CAG setup is skipped with a warning (cloud) or a plain info line (SonarQube Server). Any error in the check is treated as "not entitled". The `sonar context` passthrough is not gated — CAG itself enforces entitlement per-request. +Before installing, `sonar integrate claude|copilot|codex|antigravity` pre-flights the CAG entitlement check: `SonarQubeClient.hasCagEntitlement(orgKey)` resolves the org UUID via `/organizations/organizations` then calls `GET /a3s-analysis/cag-entitlement/{uuid}` (SonarQube Cloud only). If `allowed` is false, CAG setup is skipped with a warning (cloud) or a plain info line (SonarQube Server). Any error in the check is treated as "not entitled". The `sonar context` passthrough is not gated — CAG itself enforces entitlement per-request. After the CAG entitlement check passes, the integrate flow also queries SCA availability via `SonarQubeClient.getScaEnablement(connectionType, orgKey)` (`/sca/feature-enabled` on cloud, `/api/v2/sca/feature-enabled` on SonarQube Server). The resolved boolean is passed to `sonar-context-augmentation tool print-skill` as `--sca-enabled=true|false`, and is persisted on declarative feature attrs together with the recorded `serverUrl`, `orgKey`, and `projectKey` so `sonar context` and post-update can reuse the same connection metadata without re-querying the server. A check failure (network/404) emits a warn line and proceeds with `--sca-enabled=false`. diff --git a/src/cli/commands/integrate/_common/context-augmentation.ts b/src/cli/commands/integrate/_common/context-augmentation.ts index 09307d221..0d1ee6278 100644 --- a/src/cli/commands/integrate/_common/context-augmentation.ts +++ b/src/cli/commands/integrate/_common/context-augmentation.ts @@ -107,7 +107,7 @@ export async function resolveContextAugmentationSetup( // that could not use CAG anyway). if (p.auth.orgKey) { const client = new SonarQubeClient(p.auth.serverUrl, p.auth.token); - if ((await client.hasCagEntitlement(p.auth.orgKey)) === 'enabled') { + if ((await client.hasCagEntitlement(p.auth.orgKey)) === 'allowed') { warn( 'Skipping Context Augmentation: not supported with --global. Re-run without --global from a project directory to install it there.', ); @@ -131,9 +131,9 @@ export async function resolveContextAugmentationSetup( ); return null; } - if (entitlement === 'not_enabled') { + if (entitlement === 'not_allowed') { warn( - 'Skipping Context Augmentation: not enabled for your organization. Enable it in your SonarQube Cloud organization settings.', + 'Skipping Context Augmentation: not available for your organization. Access requires an eligible SonarQube Cloud plan.', ); return null; } diff --git a/src/sonarqube/client.ts b/src/sonarqube/client.ts index 5c8d5a0c2..1dbfad05f 100644 --- a/src/sonarqube/client.ts +++ b/src/sonarqube/client.ts @@ -48,7 +48,7 @@ export const GENERIC_HTTP_METHODS = ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'] as export const METHODS_WITH_BODY = new Set(['POST', 'PATCH', 'PUT']); export type HttpMethod = (typeof GENERIC_HTTP_METHODS)[number]; -export type CagEntitlementStatus = 'enabled' | 'not_enabled' | 'check_failed'; +export type CagEntitlementStatus = 'allowed' | 'not_allowed' | 'check_failed'; export type SqaaEntitlementStatus = 'enabled' | 'not_enabled' | 'check_failed'; @@ -392,13 +392,13 @@ export class SonarQubeClient { async checkCagEntitlement(organizationUuid: string): Promise { try { - const endpoint = `/a3s-analysis/cag-org-config/${organizationUuid}`; - const result = await this.get<{ id: string; enabled: boolean; eligible: boolean }>( + const endpoint = `/a3s-analysis/cag-entitlement/${organizationUuid}`; + const result = await this.get<{ allowed: boolean; consumption?: object }>( endpoint, undefined, resolveFromEndpoint(this.serverURL, endpoint), ); - return result.eligible && result.enabled ? 'enabled' : 'not_enabled'; + return result.allowed ? 'allowed' : 'not_allowed'; } catch { return 'check_failed'; } @@ -406,7 +406,7 @@ export class SonarQubeClient { async hasCagEntitlement(organizationKey?: string): Promise { if (!organizationKey || !isSonarQubeCloud(this.serverURL)) { - return 'not_enabled'; + return 'not_allowed'; } const uuid = await this.getOrganizationId(organizationKey); if (!uuid) { diff --git a/tests/e2e/context/cag-integrate.test.ts b/tests/e2e/context/cag-integrate.test.ts index 983ec69a2..19c4df0e7 100644 --- a/tests/e2e/context/cag-integrate.test.ts +++ b/tests/e2e/context/cag-integrate.test.ts @@ -24,7 +24,7 @@ * the real CAG binary at all on connections where CAG must be skipped: * * - SonarQube Server (non-Cloud) connections. - * - Cloud connections where the org's CAG entitlement is disabled. + * - Cloud connections where the org is not allowed to use CAG. * * Both paths must exit before `installContextAugmentationBinary()` runs, so * we assert no binary lands under `/bin/` and no declarative CAG @@ -103,12 +103,12 @@ describe('sonar integrate — CAG pre-flight skip paths (real CLI, fake expect(findRecordedCagFeature(harness.stateJsonFile.asJson() as CliState)).toBeUndefined(); }); - it('skips CAG when entitlement is not enabled on the Cloud org', async () => { + it('skips CAG when the Cloud org is not allowed to use it', async () => { const server = await harness .newFakeServer() .withAuthToken(TOKEN) .withProject(PROJECT_KEY) - .withCagEntitlement(ORG_KEY, { enabled: false }) + .withCagEntitlement(ORG_KEY, { allowed: false }) .start(); const serverUrl = server.baseUrl(); harness.withAuth(serverUrl, TOKEN, ORG_KEY); @@ -132,9 +132,9 @@ describe('sonar integrate — CAG pre-flight skip paths (real CLI, fake expect(result.exitCode, result.stderr).toBe(0); expect(result.stderr).toContain( - 'Skipping Context Augmentation: not enabled for your organization. Enable it in your SonarQube Cloud organization settings.', + 'Skipping Context Augmentation: not available for your organization. Access requires an eligible SonarQube Cloud plan.', ); - expect(existsSync(cagBinaryPath), 'no CAG download when entitlement is disabled').toBe(false); + expect(existsSync(cagBinaryPath), 'no CAG download when access is denied').toBe(false); expect(findRecordedCagFeature(harness.stateJsonFile.asJson() as CliState)).toBeUndefined(); }); }); diff --git a/tests/integration/harness/fake-sonarqube-server.test.ts b/tests/integration/harness/fake-sonarqube-server.test.ts index 68ed0f63f..2401869de 100644 --- a/tests/integration/harness/fake-sonarqube-server.test.ts +++ b/tests/integration/harness/fake-sonarqube-server.test.ts @@ -32,19 +32,7 @@ describe('FakeSonarQubeServer — UUID consistency', () => { await server?.stop(); }); - it('returns cag-org-config 200 when org has both SQAA and CAG entitlement with a custom UUID', async () => { - // This test demonstrates the UUID mismatch bug: - // - // withSqaaEntitlement stores an explicit UUID ('test-uuid-1234'). - // withCagEntitlement stores no UUID — the cag-org-config lookup derives it - // as `${orgKey}-uuid-v4`. - // - // When CAG calls /organizations/organizations (no param), the no-param - // branch synthesises org entries taking the UUID from SQAA ('test-uuid-1234'). - // CAG then calls /a3s-analysis/cag-org-config/test-uuid-1234, but the handler - // looks for an org where `${orgKey}-uuid-v4 === 'test-uuid-1234'`, which never - // matches — so it returns 404 and CAG treats itself as not entitled. - + it('returns cag-entitlement 200 when org has both SQAA and CAG entitlement', async () => { server = await new FakeSonarQubeServerBuilder() .withSqaaEntitlement('my-org', 'test-uuid-1234') .withCagEntitlement('my-org') @@ -61,17 +49,12 @@ describe('FakeSonarQubeServer — UUID consistency', () => { const uuid = orgs[0].uuidV4; - // Step 2: fetch cag-org-config using the UUID CAG received from step 1. - const configResp = await fetch(`${base}/a3s-analysis/cag-org-config/${uuid}`); + // Step 2: fetch CAG entitlement using the UUID CAG received from step 1. + const configResp = await fetch(`${base}/a3s-analysis/cag-entitlement/${uuid}`); - // Expected: 200 with eligible/enabled from withCagEntitlement. - // Actual (bug): 404 because the handler derives UUID as `${orgKey}-uuid-v4` - // but the UUID returned by /organizations/organizations is 'test-uuid-1234' - // (taken from SQAA), not 'my-org-uuid-v4'. expect(configResp.status).toBe(HTTP_OK); - const config = (await configResp.json()) as { eligible: boolean; enabled: boolean }; - expect(config.eligible).toBe(true); - expect(config.enabled).toBe(true); + const config = (await configResp.json()) as { allowed: boolean }; + expect(config.allowed).toBe(true); }); }); diff --git a/tests/integration/harness/fake-sonarqube-server.ts b/tests/integration/harness/fake-sonarqube-server.ts index aa9b973ba..a5e5d3ebc 100644 --- a/tests/integration/harness/fake-sonarqube-server.ts +++ b/tests/integration/harness/fake-sonarqube-server.ts @@ -121,10 +121,7 @@ export class FakeSonarQubeServerBuilder { string, { uuid: string; eligible: boolean; enabled: boolean } > = new Map(); - private readonly cagEntitlementOrgs: Map< - string, - { uuid: string; eligible: boolean; enabled: boolean } - > = new Map(); + private readonly cagEntitlementOrgs: Map = new Map(); private validToken?: string; private systemStatusCode = 200; private systemVersion = '9.9.0.00001'; @@ -137,6 +134,8 @@ export class FakeSonarQubeServerBuilder { private sqaaStatusBody?: string; private sqaaPayloadLimit?: { maxRequestSize?: number; maxFiles?: number }; private scaEnabled?: boolean; + private cagEntitlementStatusCode?: number; + private cagEntitlementStatusBody?: string; private readonly projectSettings: Map = new Map(); private agentJobErrorCode?: number; private agentJobErrorMessage?: string; @@ -254,18 +253,24 @@ export class FakeSonarQubeServerBuilder { return this; } - withCagEntitlement( - orgKey: string, - options: { uuid?: string; eligible?: boolean; enabled?: boolean } = {}, - ): this { + withCagEntitlement(orgKey: string, options: { uuid?: string; allowed?: boolean } = {}): this { this.cagEntitlementOrgs.set(orgKey, { uuid: options.uuid ?? `${orgKey}-uuid-v4`, - eligible: options.eligible ?? true, - enabled: options.enabled ?? true, + allowed: options.allowed ?? true, }); return this; } + /** + * Force GET /a3s-analysis/cag-entitlement/{uuid} to return a specific HTTP + * status code. Useful for testing entitlement check failure paths. + */ + withCagEntitlementStatusCode(status: number, body?: string): this { + this.cagEntitlementStatusCode = status; + this.cagEntitlementStatusBody = body; + return this; + } + /** * Configure the response of the SCA availability endpoints * (`/sca/feature-enabled` for cloud, `/api/v2/sca/feature-enabled` for on-premise). @@ -305,6 +310,8 @@ export class FakeSonarQubeServerBuilder { sqaaEntitlementOrgs, cagEntitlementOrgs, scaEnabled, + cagEntitlementStatusCode, + cagEntitlementStatusBody, projectSettings, agentJobErrorCode, agentJobErrorMessage, @@ -557,11 +564,13 @@ export class FakeSonarQubeServerBuilder { }); } const orgKey = query.organizationKey; - const entitlement = orgKey ? sqaaEntitlementOrgs.get(orgKey) : undefined; - if (entitlement) { + const sqaaEntitlement = orgKey ? sqaaEntitlementOrgs.get(orgKey) : undefined; + const cagEntitlement = orgKey ? cagEntitlementOrgs.get(orgKey) : undefined; + const entitlementUuid = cagEntitlement?.uuid ?? sqaaEntitlement?.uuid; + if (orgKey && entitlementUuid) { return new Response( JSON.stringify([ - { id: `id-${orgKey}`, uuidV4: entitlement.uuid, key: orgKey, name: orgKey }, + { id: `id-${orgKey}`, uuidV4: entitlementUuid, key: orgKey, name: orgKey }, ]), { headers: { 'Content-Type': 'application/json' } }, ); @@ -577,9 +586,9 @@ export class FakeSonarQubeServerBuilder { { headers: { 'Content-Type': 'application/json' } }, ); } - // No organizationKey query param: sonar-context-augmentation calls - // /organizations/organizations with no params during open-beta - // entitlement resolution and expects a flat list of accessible orgs. + // No organizationKey query param: sonar-context-augmentation can call + // /organizations/organizations with no params during entitlement + // resolution and expects a flat list of accessible orgs. // Synthesize entries from the orgs that have CAG/SQAA entitlement // registered so the daemon can find a matching org by key. const knownOrgs = new Set([ @@ -653,9 +662,18 @@ export class FakeSonarQubeServerBuilder { ); } - const cagOrgConfigMatch = /^\/a3s-analysis\/cag-org-config\/(.+)$/.exec(path); - if (cagOrgConfigMatch) { - const uuid = cagOrgConfigMatch[1]; + const cagEntitlementMatch = /^\/a3s-analysis\/cag-entitlement\/(.+)$/.exec(path); + if (cagEntitlementMatch) { + if (cagEntitlementStatusCode !== undefined) { + return new Response( + cagEntitlementStatusBody ?? JSON.stringify({ errors: [{ msg: 'CAG failed' }] }), + { + status: cagEntitlementStatusCode, + headers: { 'Content-Type': 'application/json' }, + }, + ); + } + const uuid = cagEntitlementMatch[1]; const entitlement = [...cagEntitlementOrgs.values()].find((e) => e.uuid === uuid); if (!entitlement) { return new Response(JSON.stringify({ errors: [{ msg: 'Not found' }] }), { @@ -665,9 +683,7 @@ export class FakeSonarQubeServerBuilder { } return new Response( JSON.stringify({ - id: uuid, - eligible: entitlement.eligible, - enabled: entitlement.enabled, + allowed: entitlement.allowed, }), { headers: { 'Content-Type': 'application/json' } }, ); diff --git a/tests/integration/specs/integrate/context-augmentation.test.ts b/tests/integration/specs/integrate/context-augmentation.test.ts index 4d499fc5c..470bc3884 100644 --- a/tests/integration/specs/integrate/context-augmentation.test.ts +++ b/tests/integration/specs/integrate/context-augmentation.test.ts @@ -333,13 +333,13 @@ describe('integrate claude — Context Augmentation', () => { ); it( - 'skips CAG with a warning when the org does not have it enabled', + 'skips CAG with a warning when the org is not allowed to use it', async () => { const server = await harness .newFakeServer() .withAuthToken(TOKEN) .withProject(PROJECT_KEY) - .withCagEntitlement(ORG_KEY, { enabled: false }) + .withCagEntitlement(ORG_KEY, { allowed: false }) .start(); const serverUrl = server.baseUrl(); harness.withAuth(serverUrl, TOKEN, ORG_KEY); @@ -366,7 +366,46 @@ describe('integrate claude — Context Augmentation', () => { const state = loadState(harness); expect(findRecordedCagFeature(state)).toBeUndefined(); expect(harness.cwd.file(CLAUDE_SKILL_PATH).exists()).toBe(false); - expect(result.stderr).toContain('not enabled for your organization'); + expect(result.stderr).toContain('not available for your organization'); + }, + { timeout: 30000 }, + ); + + it( + 'skips CAG with a warning when the entitlement check fails', + async () => { + const server = await harness + .newFakeServer() + .withAuthToken(TOKEN) + .withProject(PROJECT_KEY) + .withCagEntitlementStatusCode(500) + .start(); + const serverUrl = server.baseUrl(); + harness.withAuth(serverUrl, TOKEN, ORG_KEY); + harness.state().withContextAugmentationBinaryInstalled(); + harness.cwd.writeFile( + 'sonar-project.properties', + [ + `sonar.host.url=${serverUrl}`, + `sonar.projectKey=${PROJECT_KEY}`, + `sonar.organization=${ORG_KEY}`, + ].join('\n'), + ); + + const result = await harness.run('integrate claude --non-interactive', { + extraEnv: { + SONARQUBE_CLI_SONARCLOUD_URL: serverUrl, + SONARQUBE_CLI_SONARCLOUD_API_URL: serverUrl, + }, + }); + + expect(result.exitCode).toBe(0); + const nonProbe = readInvocations(harness).filter((i) => i.argv[0] !== '--version'); + expect(nonProbe).toEqual([]); + const state = loadState(harness); + expect(findRecordedCagFeature(state)).toBeUndefined(); + expect(harness.cwd.file(CLAUDE_SKILL_PATH).exists()).toBe(false); + expect(result.stderr).toContain('could not verify entitlement'); }, { timeout: 30000 }, ); @@ -774,13 +813,13 @@ describe('integrate codex — Context Augmentation', () => { ); it( - 'skips CAG with a warning when the org does not have it enabled', + 'skips CAG with a warning when the org is not allowed to use it', async () => { const server = await harness .newFakeServer() .withAuthToken(TOKEN) .withProject(PROJECT_KEY) - .withCagEntitlement(ORG_KEY, { enabled: false }) + .withCagEntitlement(ORG_KEY, { allowed: false }) .start(); const serverUrl = server.baseUrl(); harness.withAuth(serverUrl, TOKEN, ORG_KEY); @@ -805,7 +844,7 @@ describe('integrate codex — Context Augmentation', () => { const nonProbe = readInvocations(harness).filter((i) => i.argv[0] !== '--version'); expect(nonProbe).toEqual([]); expect(harness.cwd.file(CODEX_SKILL_PATH).exists()).toBe(false); - expect(result.stderr).toContain('not enabled for your organization'); + expect(result.stderr).toContain('not available for your organization'); }, { timeout: 30000 }, ); diff --git a/tests/unit/cli/commands/integrate/claude/integrate.test.ts b/tests/unit/cli/commands/integrate/claude/integrate.test.ts index c79c719aa..687825e1f 100644 --- a/tests/unit/cli/commands/integrate/claude/integrate.test.ts +++ b/tests/unit/cli/commands/integrate/claude/integrate.test.ts @@ -101,7 +101,7 @@ describe('integrateCommand', () => { hasSqaaEntitlementSpy = spyOn(SonarQubeClient.prototype, 'hasSqaaEntitlement'); hasSqaaEntitlementSpy.mockResolvedValue('not_enabled'); hasCagEntitlementSpy = spyOn(SonarQubeClient.prototype, 'hasCagEntitlement'); - hasCagEntitlementSpy.mockResolvedValue('enabled'); + hasCagEntitlementSpy.mockResolvedValue('allowed'); resolveContextAugmentationSetupSpy = spyOn( contextAugmentation, 'resolveContextAugmentationSetup', diff --git a/tests/unit/sonarqube/client.test.ts b/tests/unit/sonarqube/client.test.ts index 880ad9bd2..39aaf8fca 100644 --- a/tests/unit/sonarqube/client.test.ts +++ b/tests/unit/sonarqube/client.test.ts @@ -817,22 +817,22 @@ describe('SonarQubeClient', () => { cloudClient = new SonarQubeClient(SONARCLOUD_URL, TOKEN); }); - it('returns not_enabled when organizationKey is not provided', async () => { + it('returns not_allowed when organizationKey is not provided', async () => { fetchSpy = mockFetch({}); - expect(await cloudClient.hasCagEntitlement(undefined)).toBe('not_enabled'); + expect(await cloudClient.hasCagEntitlement(undefined)).toBe('not_allowed'); expect(fetchSpy).not.toHaveBeenCalled(); }); - it('returns not_enabled when organizationKey is empty string', async () => { + it('returns not_allowed when organizationKey is empty string', async () => { fetchSpy = mockFetch({}); - expect(await cloudClient.hasCagEntitlement('')).toBe('not_enabled'); + expect(await cloudClient.hasCagEntitlement('')).toBe('not_allowed'); expect(fetchSpy).not.toHaveBeenCalled(); }); - it('returns not_enabled when server is not SonarQube Cloud', async () => { + it('returns not_allowed when server is not SonarQube Cloud', async () => { const serverClient = new SonarQubeClient(SERVER_URL, TOKEN); fetchSpy = mockFetch({}); - expect(await serverClient.hasCagEntitlement('my-org')).toBe('not_enabled'); + expect(await serverClient.hasCagEntitlement('my-org')).toBe('not_allowed'); expect(fetchSpy).not.toHaveBeenCalled(); }); @@ -841,7 +841,7 @@ describe('SonarQubeClient', () => { expect(await cloudClient.hasCagEntitlement('unknown-org')).toBe('check_failed'); }); - it('returns enabled when org UUID is resolved and entitlement is eligible and enabled', async () => { + it('returns allowed when org UUID is resolved and entitlement is allowed', async () => { fetchSpy = spyOn(globalThis, 'fetch') .mockResolvedValueOnce({ ok: true, @@ -851,13 +851,13 @@ describe('SonarQubeClient', () => { .mockResolvedValueOnce({ ok: true, status: 200, - json: () => Promise.resolve({ id: 'org-uuid', eligible: true, enabled: true }), + json: () => Promise.resolve({ allowed: true }), } as Response); - expect(await cloudClient.hasCagEntitlement('my-org')).toBe('enabled'); + expect(await cloudClient.hasCagEntitlement('my-org')).toBe('allowed'); }); - it('returns not_enabled when eligible but not enabled', async () => { + it('returns not_allowed when the organization is not allowed', async () => { fetchSpy = spyOn(globalThis, 'fetch') .mockResolvedValueOnce({ ok: true, @@ -867,13 +867,13 @@ describe('SonarQubeClient', () => { .mockResolvedValueOnce({ ok: true, status: 200, - json: () => Promise.resolve({ id: 'org-uuid', eligible: true, enabled: false }), + json: () => Promise.resolve({ allowed: false }), } as Response); - expect(await cloudClient.hasCagEntitlement('my-org')).toBe('not_enabled'); + expect(await cloudClient.hasCagEntitlement('my-org')).toBe('not_allowed'); }); - it('returns not_enabled when enabled but not eligible', async () => { + it('ignores consumption data when mapping an allowed response', async () => { fetchSpy = spyOn(globalThis, 'fetch') .mockResolvedValueOnce({ ok: true, @@ -883,10 +883,10 @@ describe('SonarQubeClient', () => { .mockResolvedValueOnce({ ok: true, status: 200, - json: () => Promise.resolve({ id: 'org-uuid', eligible: false, enabled: true }), + json: () => Promise.resolve({ allowed: true, consumption: { consumed: 10, limit: 100 } }), } as Response); - expect(await cloudClient.hasCagEntitlement('my-org')).toBe('not_enabled'); + expect(await cloudClient.hasCagEntitlement('my-org')).toBe('allowed'); }); it('returns check_failed when the entitlement check fails with an API error', async () => { @@ -906,7 +906,7 @@ describe('SonarQubeClient', () => { expect(await cloudClient.hasCagEntitlement('my-org')).toBe('check_failed'); }); - it('hits the correct CAG org config endpoint with the resolved UUID', async () => { + it('hits the correct CAG entitlement endpoint with the resolved UUID', async () => { const targetUuid = 'cag-specific-uuid'; fetchSpy = spyOn(globalThis, 'fetch') .mockResolvedValueOnce({ @@ -917,13 +917,13 @@ describe('SonarQubeClient', () => { .mockResolvedValueOnce({ ok: true, status: 200, - json: () => Promise.resolve({ id: targetUuid, eligible: true, enabled: true }), + json: () => Promise.resolve({ allowed: true }), } as Response); await cloudClient.hasCagEntitlement('my-org'); const entitlementUrl = new URL((fetchSpy.mock.calls[1][0] as URL).toString()); - expect(entitlementUrl.pathname).toBe(`/a3s-analysis/cag-org-config/${targetUuid}`); + expect(entitlementUrl.pathname).toBe(`/a3s-analysis/cag-entitlement/${targetUuid}`); }); });