import { nanoid } from 'nanoid' import * as TestCli from '../../../test/cli.js' import * as Analytics from '../../analytics/Analytics.js' afterEach(() => vi.unstubAllGlobals()) describe('admin', () => { describe('migrate', () => { test('behavior: applies migrations', async () => { const { code, envelope } = await TestCli.run([ 'admin', 'migrate', '--db-schema', `t_${nanoid()}`, ]) expect(code).toBe(0) expect(envelope).toMatchInlineSnapshot(` { "data": { "migrated": true, }, "ok": true, } `) }) test('behavior: applies migrations from DATABASE_URL without a schema option', async () => { const { code, envelope } = await TestCli.run(['admin', 'migrate']) expect(code).toBe(0) expect(envelope).toEqual({ data: { migrated: true }, ok: true }) }) test('behavior: is idempotent across runs', async () => { const schema = `t_${nanoid()}` expect((await TestCli.run(['admin', 'migrate', '--db-schema', schema])).code).toBe(0) const { code, envelope } = await TestCli.run(['admin', 'migrate', '--db-schema', schema]) expect(code).toBe(0) expect(envelope).toEqual({ data: { migrated: true }, ok: true }) }) test('error: a missing connection string exits 1', async () => { const { code, envelope } = await TestCli.run(['admin', 'migrate'], { DATABASE_URL: undefined, }) expect(code).toBe(1) expect(envelope).toMatchInlineSnapshot(` { "error": { "code": "missing_database_url", "message": "Set --database-url or DATABASE_URL to the Postgres connection string.", }, "ok": false, } `) }) test('error: a migration failure exits 1', async () => { const { code, envelope } = await TestCli.run( [ 'admin', 'migrate', '--database-url', 'postgres://postgres:postgres@127.0.0.1:1/tempo-api', '--db-schema', `t_${nanoid()}`, ], { DATABASE_URL: undefined }, ) expect(code).toBe(1) expect(envelope.error.code).toBe('migrate_failed') }) test('behavior: applies ClickHouse analytics migrations when configured', async () => { const fetch = vi .fn() .mockResolvedValue(new Response('', { status: 200 })) vi.stubGlobal('fetch', fetch) const { code, envelope } = await TestCli.run( ['admin', 'migrate', '--db-schema', `t_${nanoid()}`], { CLICKHOUSE_DATABASE: 'tempo_api', CLICKHOUSE_MIGRATE_PASSWORD: 'ddl-secret', CLICKHOUSE_MIGRATE_USER: 'ddl', CLICKHOUSE_URL: 'https://clickhouse.test', }, ) expect(code).toBe(0) expect(envelope).toEqual({ data: { analytics: true, migrated: true }, ok: true }) const url = new URL(String(fetch.mock.calls[0]![0])) expect(url.searchParams.get('database')).toBe('tempo_api') const bodies = fetch.mock.calls.map((call) => String(call[1]?.body)) for (const migration of Analytics.migrations) expect(bodies).toContain(migration.sql) }) test('behavior: skips analytics migrations when ClickHouse env is unset or empty', async () => { const fetch = vi.fn() vi.stubGlobal('fetch', fetch) const { code, envelope } = await TestCli.run( ['admin', 'migrate', '--db-schema', `t_${nanoid()}`], { CLICKHOUSE_DATABASE: '', CLICKHOUSE_MIGRATE_PASSWORD: '', CLICKHOUSE_MIGRATE_USER: '', CLICKHOUSE_URL: '', }, ) expect(code).toBe(0) expect(envelope).toEqual({ data: { migrated: true }, ok: true }) expect(fetch).not.toHaveBeenCalled() }) test('error: partial ClickHouse config exits 1', async () => { const { code, envelope } = await TestCli.run(['admin', 'migrate'], { CLICKHOUSE_MIGRATE_PASSWORD: 'ddl-secret', }) expect(code).toBe(1) expect(envelope).toEqual({ error: { code: 'missing_clickhouse_config', message: 'ClickHouse analytics migration is partially configured; set CLICKHOUSE_DATABASE, CLICKHOUSE_MIGRATE_USER, CLICKHOUSE_URL.', }, ok: false, }) }) test('error: a ClickHouse migration failure exits 1', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue(new Response('denied', { status: 403 })), ) const { code, envelope } = await TestCli.run([ 'admin', 'migrate', '--clickhouse-database', 'tempo_api', '--clickhouse-migrate-password', 'ddl-secret', '--clickhouse-migrate-user', 'ddl', '--clickhouse-url', 'https://clickhouse.test', '--db-schema', `t_${nanoid()}`, ]) expect(code).toBe(1) expect(envelope.error.code).toBe('migrate_failed') }) }) })