import { afterAll, beforeAll, describe, expect, it } from 'vitest' import { InMemoryKVDriver } from '../../src/kv/memory.js' import type { KVDriver } from '../../src/kv/driver.js' import { KVStorage } from '../../src/kv/node.js' import { KVEventStorage } from '../../src/events/kv.js' import { EventSourcedDocumentStorage } from '../../src/documents/event-sourced.js' import { createKVHttpHandler } from '../../src/http/handler.js' import { HttpKVDriver } from '../../src/http/client.js' import { runKVDriverConformance } from '../../src/testing/kv-conformance.js' import { collect, CountingDriver, MinimalDriver } from '../helpers.js' import { serve } from './node-server.js' /** A client wired straight to a handler, no socket. */ function inProcess(server: KVDriver = new InMemoryKVDriver(), caps?: ConstructorParameters[0]['capabilities']) { const handler = createKVHttpHandler(server, { basePath: '/kv' }) return new HttpKVDriver({ url: 'http://test/kv', capabilities: caps, fetch: async (input, init) => handler(new Request(input, init)) }) } runKVDriverConformance('HttpKVDriver (in-process)', () => inProcess()) runKVDriverConformance('HttpKVDriver (minimal server driver, discovered capabilities)', () => { const handler = createKVHttpHandler(new MinimalDriver()) return HttpKVDriver.connect({ url: 'http://test', fetch: async (input, init) => handler(new Request(input, init)) }) }) describe('over a real socket', () => { let server: Awaited> const backends: InMemoryKVDriver[] = [] beforeAll(async () => { // One server; each driver gets a fresh backend selected by path. server = await serve(async (req) => { const m = /^\/db(\d+)/.exec(new URL(req.url).pathname) const backend = backends[Number(m?.[1])] if (!backend) return new Response('no db', { status: 404 }) return createKVHttpHandler(backend, { basePath: `/db${m![1]}` })(req) }) }) afterAll(() => server.close()) const fresh = () => { backends.push(new InMemoryKVDriver()) return HttpKVDriver.connect({ url: `${server.url}/db${backends.length - 1}` }) } runKVDriverConformance('HttpKVDriver (node:http)', fresh) it('cancels a server-side scan when the client stops early', async () => { const d = await fresh() await d.setMany(Array.from({ length: 5000 }, (_, i) => [`k${String(i).padStart(5, '0')}`, i] as [string, number])) const first: string[] = [] for await (const [k] of d.scan({})) { first.push(k) if (first.length === 3) break } expect(first).toEqual(['k00000', 'k00001', 'k00002']) }) it('runs the whole stack (KV indexes, events, documents) over HTTP', async () => { const kv = new KVStorage(await fresh(), { indexes: { users: { byAge: (u: { age: number }) => u.age } } }) await kv.child('users').set('1', { age: 30 }) expect(await collect(kv.child('users').findBy('byAge', { gte: 18 }))).toEqual([['1', { age: 30 }]]) const docs = new EventSourcedDocumentStorage(new KVEventStorage(kv), { indexes: { people: [{ fields: ['email'], unique: true }] } }) const id = await docs.child('people').insert({ email: 'a@x', n: 1 }) await docs.child('people').update(id, { $inc: { n: 1 } }) await expect(docs.child('people').insert({ email: 'a@x' })).rejects.toThrow(/Unique/) expect(await docs.child('people').findOne({ email: 'a@x' })).toEqual({ key: id, data: { email: 'a@x', n: 2 } }) }) }) describe('handler', () => { it('reports capabilities and rejects unsupported operations with 501', async () => { const handler = createKVHttpHandler(new MinimalDriver()) const caps = await (await handler(new Request('http://x/capabilities'))).json() expect(caps).toEqual({ setIfAbsent: false, batch: false, deleteRange: true }) const res = await handler(new Request('http://x/batch', { method: 'POST', body: JSON.stringify({ ops: [] }) })) expect(res.status).toBe(501) }) it('maps driver errors to 500 and unknown routes to 404', async () => { const broken = new CountingDriver(new InMemoryKVDriver()) broken.getMany = () => Promise.reject(new Error('boom')) const handler = createKVHttpHandler(broken) expect((await handler(new Request('http://x/getMany', { method: 'POST', body: '{"keys":["a"]}' }))).status).toBe(500) expect((await handler(new Request('http://x/nope'))).status).toBe(404) await expect(inProcess(new MinimalDriver(), { batch: true }).batch!([])).rejects.toThrow(/HTTP 501/) }) it('distinguishes null values from missing keys', async () => { const d = inProcess() await d.setMany([['n', null]]) expect(await d.getMany(['n', 'missing'])).toEqual([null, undefined]) }) })