From c439feb50f054adae8488f016ab01b2d42ec9b26 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Thu, 18 Jun 2026 13:35:09 +0200 Subject: [PATCH] fix(cli): resolve Looker client_secret_ref via the canonical config resolver The local Looker adapter understood only env: references, so a client_secret_ref written by `ktx setup` as a file: secret silently failed to resolve. Route it through resolveKtxConfigReference so file: and env: refs both work, matching the other connector paths. --- .../adapters/looker/local-looker.adapter.ts | 11 +-- .../looker/local-looker.adapter.test.ts | 72 +++++++++++++++++++ 2 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 packages/cli/test/context/ingest/adapters/looker/local-looker.adapter.test.ts diff --git a/packages/cli/src/context/ingest/adapters/looker/local-looker.adapter.ts b/packages/cli/src/context/ingest/adapters/looker/local-looker.adapter.ts index ea8ba658..b0dd5c53 100644 --- a/packages/cli/src/context/ingest/adapters/looker/local-looker.adapter.ts +++ b/packages/cli/src/context/ingest/adapters/looker/local-looker.adapter.ts @@ -1,3 +1,4 @@ +import { resolveKtxConfigReference } from '../../../core/config-reference.js'; import type { KtxLocalProject } from '../../../../context/project/project.js'; import type { KtxProjectConnectionConfig } from '../../../../context/project/config.js'; import { @@ -8,13 +9,6 @@ function stringField(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null; } -function resolveEnvReference(ref: string, env: NodeJS.ProcessEnv): string | null { - if (!ref.startsWith('env:')) { - return null; - } - return stringField(env[ref.slice('env:'.length)]); -} - export function lookerCredentialsFromLocalConnection( connectionId: string, connection: KtxProjectConnectionConfig | undefined, @@ -27,7 +21,8 @@ export function lookerCredentialsFromLocalConnection( const clientId = stringField(connection.client_id); const clientSecret = stringField(connection.client_secret) ?? - (stringField(connection.client_secret_ref) ? resolveEnvReference(String(connection.client_secret_ref), env) : null); + resolveKtxConfigReference(stringField(connection.client_secret_ref) ?? undefined, env) ?? + null; if (!baseUrl) { throw new Error(`Connection "${connectionId}" is missing Looker base_url`); diff --git a/packages/cli/test/context/ingest/adapters/looker/local-looker.adapter.test.ts b/packages/cli/test/context/ingest/adapters/looker/local-looker.adapter.test.ts new file mode 100644 index 00000000..3cbed033 --- /dev/null +++ b/packages/cli/test/context/ingest/adapters/looker/local-looker.adapter.test.ts @@ -0,0 +1,72 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { lookerCredentialsFromLocalConnection } from '../../../../../src/context/ingest/adapters/looker/local-looker.adapter.js'; +import type { KtxProjectConnectionConfig } from '../../../../../src/context/project/config.js'; + +const connectionId = '11111111-1111-4111-8111-111111111111'; + +function lookerConnection(overrides: Partial): KtxProjectConnectionConfig { + return { + driver: 'looker', + base_url: 'https://looker.example.com', + client_id: 'client-123', + ...overrides, + } as KtxProjectConnectionConfig; +} + +describe('lookerCredentialsFromLocalConnection', () => { + let secretsDir: string; + + beforeEach(async () => { + secretsDir = await mkdtemp(join(tmpdir(), 'looker-secret-')); + }); + + afterEach(async () => { + await rm(secretsDir, { recursive: true, force: true }); + }); + + it('resolves client_secret_ref written as a local secret file (ktx setup default)', async () => { + const secretPath = join(secretsDir, 'looker-main-client-secret'); + await writeFile(secretPath, 'file-secret\n', 'utf-8'); + + const credentials = lookerCredentialsFromLocalConnection( + connectionId, + lookerConnection({ client_secret_ref: `file:${secretPath}` }), // pragma: allowlist secret + {}, + ); + + expect(credentials.client_secret).toBe('file-secret'); + }); + + it('resolves client_secret_ref from the environment', () => { + const credentials = lookerCredentialsFromLocalConnection( + connectionId, + lookerConnection({ client_secret_ref: 'env:LOOKER_CLIENT_SECRET' }), // pragma: allowlist secret + { LOOKER_CLIENT_SECRET: 'env-secret' }, // pragma: allowlist secret + ); + + expect(credentials.client_secret).toBe('env-secret'); + }); + + it('prefers a literal client_secret over the reference', () => { + const credentials = lookerCredentialsFromLocalConnection( + connectionId, + lookerConnection({ client_secret: 'literal-secret', client_secret_ref: 'env:UNSET' }), // pragma: allowlist secret + {}, + ); + + expect(credentials.client_secret).toBe('literal-secret'); + }); + + it('throws when neither client_secret nor a resolvable client_secret_ref is present', () => { + expect(() => + lookerCredentialsFromLocalConnection( + connectionId, + lookerConnection({ client_secret_ref: 'env:UNSET' }), // pragma: allowlist secret + {}, + ), + ).toThrow(/missing Looker client_secret/); + }); +});