Files
dbmole-mcp/packages/esmole-mcp/test/unit/es/backend.test.ts
T

140 lines
5.5 KiB
TypeScript

import { createServer, type Server } from 'node:http'
import type { BackendTarget } from '@dbmole/core'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { createEsBackend } from '../../../src/es/backend.js'
import type { ConnectionConfig } from '../../../src/schema.js'
let server: Server
let port: number
const seen: { method?: string; url?: string; auth?: string; body?: string }[] = []
beforeAll(async () => {
server = createServer((req, res) => {
let body = ''
req.on('data', (c) => {
body += c
})
req.on('end', () => {
seen.push({ method: req.method, url: req.url, auth: req.headers.authorization, body })
if (req.url?.startsWith('/_fail')) {
res.writeHead(404, { 'content-type': 'application/json' })
res.end(JSON.stringify({ error: 'index_not_found' }))
return
}
if (req.url?.startsWith('/_text')) {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('plain text body')
return
}
if (req.url?.startsWith('/_empty')) {
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('')
return
}
if (req.url?.startsWith('/_brace')) {
// looks like JSON by leading brace but is not parseable
res.writeHead(200, { 'content-type': 'text/plain' })
res.end('{not json')
return
}
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ ok: true, echo: body || null }))
})
})
await new Promise<void>((r) => server.listen(0, '127.0.0.1', () => r()))
port = (server.address() as { port: number }).port
})
afterAll(async () => {
await new Promise<void>((r) => server.close(() => r()))
})
const target = (cfg: Partial<ConnectionConfig> = {}): BackendTarget<ConnectionConfig> => ({
config: {
name: 'x',
host: '127.0.0.1',
type: 'elasticsearch',
scheme: 'http',
verifyTls: false,
user: 'u',
password: 'p',
readonly: false,
...cfg
} as ConnectionConfig,
connectHost: '127.0.0.1',
connectPort: port,
serverName: '127.0.0.1'
})
describe('createEsBackend', () => {
it('sends basic auth and parses JSON', async () => {
const b = createEsBackend(target())
const res = await b.request('GET', '/_cluster/health')
expect(res.status).toBe(200)
expect(res.body).toEqual({ ok: true, echo: null })
expect(seen.at(-1)?.auth).toMatch(/^Basic /)
await b.dispose()
})
it('sends ApiKey auth when configured', async () => {
const b = createEsBackend(target({ user: undefined, password: undefined, apiKey: 'abc==' }))
await b.request('GET', '/')
expect(seen.at(-1)?.auth).toBe('ApiKey abc==')
await b.dispose()
})
it('returns status+body on 4xx without throwing', async () => {
const b = createEsBackend(target())
const res = await b.request('GET', '/_fail')
expect(res.status).toBe(404)
expect(res.body).toEqual({ error: 'index_not_found' })
await b.dispose()
})
it('serializes object bodies as JSON and sends string bodies as-is', async () => {
const b = createEsBackend(target())
await b.request('POST', '/idx/_search', { body: { query: { match_all: {} } } })
expect(JSON.parse(seen.at(-1)?.body ?? '{}')).toEqual({ query: { match_all: {} } })
await b.request('POST', '/_bulk', { body: '{"index":{}}\n{"a":1}\n' })
expect(seen.at(-1)?.body).toBe('{"index":{}}\n{"a":1}\n')
await b.dispose()
})
it('forwards query params', async () => {
const b = createEsBackend(target())
await b.request('GET', '/_cat/indices', { params: { format: 'json', v: true } })
expect(seen.at(-1)?.url).toContain('format=json')
await b.dispose()
})
it('sends no auth header when neither credentials nor apiKey are present', async () => {
const b = createEsBackend(
target({ user: undefined, password: undefined, apiKey: undefined })
)
const res = await b.request('get', 'idx/_search')
expect(res.status).toBe(200)
expect(seen.at(-1)?.auth).toBeUndefined()
// path without leading slash is normalized
expect(seen.at(-1)?.url).toBe('/idx/_search')
await b.dispose()
})
it('decodes plain text bodies and empty bodies as null', async () => {
const b = createEsBackend(target())
const text = await b.request('GET', '/_text')
expect(text.body).toBe('plain text body')
const empty = await b.request('GET', '/_empty')
expect(empty.body).toBeNull()
await b.dispose()
})
it('falls back to raw text when a JSON-looking body fails to parse', async () => {
const b = createEsBackend(target())
const res = await b.request('GET', '/_brace')
expect(res.status).toBe(200)
expect(res.body).toBe('{not json')
await b.dispose()
})
it('configures TLS servername for https origins', async () => {
// https target: factory must take the connect-config branch without a request
const b = createEsBackend(
target({ scheme: 'https', verifyTls: true, host: 'es.example.com' })
)
expect(typeof b.request).toBe('function')
await b.dispose()
})
})