From fd7a5747c7faab218eb047c9c7140da8e8f165fa Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Sun, 17 May 2026 19:01:11 +0200 Subject: [PATCH] test: validate metabase source mapping requirements --- packages/cli/src/source-mapping.test.ts | 85 +++++++++++++++++++++++++ packages/cli/src/source-mapping.ts | 9 +++ 2 files changed, 94 insertions(+) create mode 100644 packages/cli/src/source-mapping.test.ts diff --git a/packages/cli/src/source-mapping.test.ts b/packages/cli/src/source-mapping.test.ts new file mode 100644 index 00000000..83f9496b --- /dev/null +++ b/packages/cli/src/source-mapping.test.ts @@ -0,0 +1,85 @@ +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 type { KtxCliIo } from './cli-runtime.js'; +import { runKtxSourceMapping } from './source-mapping.js'; + +function makeIo() { + let stdout = ''; + let stderr = ''; + return { + io: { + stdout: { + write: (chunk: string) => { + stdout += chunk; + }, + }, + stderr: { + write: (chunk: string) => { + stderr += chunk; + }, + }, + } satisfies KtxCliIo, + stdout: () => stdout, + stderr: () => stderr, + }; +} + +describe('source mapping commands', () => { + let tempDir: string; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), 'ktx-source-mapping-')); + }); + + afterEach(async () => { + await rm(tempDir, { recursive: true, force: true }); + }); + + async function writeConfig(metabaseMappings: string[]): Promise { + await writeFile( + join(tempDir, 'ktx.yaml'), + [ + 'connections:', + ' warehouse:', + ' driver: postgres', + ' url: env:DATABASE_URL', + ' metabase:', + ' driver: metabase', + ' api_url: https://metabase.example.com', + ...metabaseMappings, + '', + ].join('\n'), + 'utf-8', + ); + } + + it('fails Metabase validation when no sync-enabled target mapping exists', async () => { + await writeConfig([]); + const io = makeIo(); + + await expect( + runKtxSourceMapping({ command: 'validate', projectDir: tempDir, connectionId: 'metabase' }, io.io), + ).resolves.toBe(1); + + expect(io.stderr()).toContain('no sync-enabled mappings with a target connection for Metabase connection metabase'); + }); + + it('passes Metabase validation when a sync-enabled target mapping exists', async () => { + await writeConfig([ + ' mappings:', + ' databaseMappings:', + ' "3": warehouse', + ' syncEnabled:', + ' "3": true', + ]); + const io = makeIo(); + + await expect( + runKtxSourceMapping({ command: 'validate', projectDir: tempDir, connectionId: 'metabase' }, io.io), + ).resolves.toBe(0); + + expect(io.stdout()).toContain('Mapping validation passed: metabase'); + }); +}); diff --git a/packages/cli/src/source-mapping.ts b/packages/cli/src/source-mapping.ts index 3f8e8782..13fa15fb 100644 --- a/packages/cli/src/source-mapping.ts +++ b/packages/cli/src/source-mapping.ts @@ -12,6 +12,7 @@ import { discoverMetabaseDatabases, lookerCredentialsFromLocalConnection, metabaseRuntimeConfigFromLocalConnection, + planMetabaseFanoutChildren, seedLocalMappingStateFromKtxYaml, validateLookerMappings, validateMappingPhysicalMatch, @@ -198,6 +199,14 @@ export async function runKtxSourceMapping( } const rows = await store.listDatabaseMappings(args.connectionId); + planMetabaseFanoutChildren({ + metabaseConnectionId: args.connectionId, + mappings: rows.map((row) => ({ + metabaseDatabaseId: row.metabaseDatabaseId, + targetConnectionId: row.targetConnectionId, + syncEnabled: row.syncEnabled, + })), + }); const failures = rows.flatMap((row) => { if (!row.targetConnectionId) { return [];