163 lines
6.9 KiB
TypeScript
163 lines
6.9 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('drops the inherited port when switching engine without a new port', () => {
|
|
writeStore(storePath, [conn('switch', { type: 'postgres', port: 5432 })])
|
|
const registry = createRegistry({ storePath, env: {} })
|
|
const updated = registry.update('switch', { type: 'mysql' })
|
|
expect(updated.config.type).toBe('mysql')
|
|
expect(updated.config.port).toBeUndefined()
|
|
})
|
|
|
|
it('keeps an explicit port when switching engine', () => {
|
|
writeStore(storePath, [conn('switch', { type: 'postgres', port: 5432 })])
|
|
const registry = createRegistry({ storePath, env: {} })
|
|
const updated = registry.update('switch', { type: 'mysql', port: 3307 })
|
|
expect(updated.config.port).toBe(3307)
|
|
})
|
|
|
|
it('clears the port when switching engine with an explicit null', () => {
|
|
writeStore(storePath, [conn('switch', { type: 'postgres', port: 5432 })])
|
|
const registry = createRegistry({ storePath, env: {} })
|
|
const updated = registry.update('switch', { type: 'mysql', port: null })
|
|
expect(updated.config.port).toBeUndefined()
|
|
})
|
|
|
|
it('keeps the port when type is unchanged', () => {
|
|
writeStore(storePath, [conn('same', { type: 'postgres', port: 5432 })])
|
|
const registry = createRegistry({ storePath, env: {} })
|
|
const updated = registry.update('same', { readonly: true })
|
|
expect(updated.config.port).toBe(5432)
|
|
})
|
|
|
|
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 })))
|
|
})
|
|
})
|