Files
dbmole-mcp/test/unit/config/registry.test.ts
T
2026-06-11 23:29:26 +05:00

134 lines
5.5 KiB
TypeScript

import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
ConnectionNotFoundError,
configHash,
createRegistry,
RegistryError
} from '../../../src/config/registry.js'
import { writeStore } from '../../../src/config/store.js'
import type { ConnectionConfig } from '../../../src/config/types.js'
const conn = (name: string, extra: Partial<ConnectionConfig> = {}): ConnectionConfig => ({
name,
type: 'postgres',
host: 'localhost',
user: 'postgres',
readonly: false,
...extra
})
describe('registry', () => {
let dir: string
let storePath: string
let configPath: string
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'dbmole-registry-'))
storePath = join(dir, 'connections.json')
configPath = join(dir, 'config.json')
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
rmSync(dir, { recursive: true, force: true })
vi.restoreAllMocks()
})
it('merges layers with env > config > store priority', () => {
writeStore(storePath, [conn('a', { host: 'from-store' }), conn('s')])
writeFileSync(
configPath,
JSON.stringify({ connections: [conn('a', { host: 'from-config' }), conn('c')] })
)
const env = { DBMOLE_CONNECTIONS: JSON.stringify([conn('a', { host: 'from-env' })]) }
const registry = createRegistry({ storePath, configPath, env })
const list = registry.list()
expect(list.map((r) => r.config.name)).toEqual(['a', 'c', 's'])
expect(registry.resolve('a').config.host).toBe('from-env')
expect(registry.resolve('a').source).toBe('env')
expect(registry.resolve('c').source).toBe('config')
expect(registry.resolve('s').source).toBe('store')
})
it('resolve throws with available names listed', () => {
writeStore(storePath, [conn('only')])
const registry = createRegistry({ storePath, env: {} })
expect(() => registry.resolve('nope')).toThrow(ConnectionNotFoundError)
expect(() => registry.resolve('nope')).toThrow(/only/)
})
it('sees external store edits without recreation (hot reload)', () => {
const registry = createRegistry({ storePath, env: {} })
expect(registry.list()).toEqual([])
writeStore(storePath, [conn('late')])
expect(registry.resolve('late').config.name).toBe('late')
})
it('add writes to store and rejects duplicates from any layer', () => {
const env = { DBMOLE_CONNECTIONS: JSON.stringify([conn('envy')]) }
const registry = createRegistry({ storePath, env })
const added = registry.add(conn('fresh'))
expect(added.source).toBe('store')
expect(registry.resolve('fresh').source).toBe('store')
expect(() => registry.add(conn('fresh'))).toThrow(RegistryError)
expect(() => registry.add(conn('envy'))).toThrow(/env/)
})
it('update patches fields, removes them via null, and revalidates', () => {
writeStore(storePath, [conn('u', { database: 'old', port: 5433 })])
const registry = createRegistry({ storePath, env: {} })
const updated = registry.update('u', { database: 'new', port: null, readonly: true })
expect(updated.config.database).toBe('new')
expect(updated.config.port).toBeUndefined()
expect(updated.config.readonly).toBe(true)
expect(registry.resolve('u').config.database).toBe('new')
expect(() => registry.update('u', { host: '' })).toThrow()
})
it('update can rename unless the new name is taken', () => {
writeStore(storePath, [conn('old-name'), conn('taken')])
const registry = createRegistry({ storePath, env: {} })
registry.update('old-name', { name: 'new-name' })
expect(registry.resolve('new-name').config.name).toBe('new-name')
expect(() => registry.resolve('old-name')).toThrow(ConnectionNotFoundError)
expect(() => registry.update('new-name', { name: 'taken' })).toThrow(RegistryError)
})
it('update and remove refuse non-store connections', () => {
const env = { DBMOLE_CONNECTIONS: JSON.stringify([conn('envy')]) }
const registry = createRegistry({ storePath, env })
expect(() => registry.update('envy', { host: 'x' })).toThrow(/env/)
expect(() => registry.remove('envy')).toThrow(/env/)
})
it('remove deletes from store', () => {
writeStore(storePath, [conn('gone'), conn('stays')])
const registry = createRegistry({ storePath, env: {} })
registry.remove('gone')
expect(() => registry.resolve('gone')).toThrow(ConnectionNotFoundError)
expect(registry.resolve('stays').config.name).toBe('stays')
})
it('update and remove of unknown names throw not-found', () => {
const registry = createRegistry({ storePath, env: {} })
expect(() => registry.update('ghost', {})).toThrow(ConnectionNotFoundError)
expect(() => registry.remove('ghost')).toThrow(ConnectionNotFoundError)
})
})
describe('configHash', () => {
it('is stable across key order', () => {
const a = conn('h', { port: 1, database: 'd' })
const b = { database: 'd', port: 1, ...conn('h') } as ConnectionConfig
expect(configHash(a)).toBe(configHash(b))
})
it('changes when a value changes', () => {
expect(configHash(conn('h'))).not.toBe(configHash(conn('h', { port: 9 })))
})
})