// Test suite for OGraphClient v2.4 import { describe, it, expect, beforeEach, vi } from 'vitest' import { OGraphClient } from '../src/client.js' // Mock the config module vi.mock('../src/config.js', () => ({ loadConfig: vi.fn(), })) import { loadConfig } from '../src/config.js' const mockLoadConfig = vi.mocked(loadConfig) // Mock fetch globally const mockFetch = vi.fn() global.fetch = mockFetch describe('OGraphClient v2.4', () => { let client: OGraphClient beforeEach(() => { client = new OGraphClient() vi.clearAllMocks() }) // ─── init ──────────────────────────────────────────────────────────────────── describe('init', () => { it('should throw error if endpoint not configured', async () => { mockLoadConfig.mockResolvedValue({}) await expect(client.init()).rejects.toThrow('API endpoint not configured. Run: ograph config set endpoint ') }) it('should throw error if token not configured', async () => { mockLoadConfig.mockResolvedValue({ endpoint: 'https://api.example.com' }) await expect(client.init()).rejects.toThrow('Auth token not configured. Run: ograph config set token ') }) it('should initialize successfully with both endpoint and token', async () => { mockLoadConfig.mockResolvedValue({ endpoint: 'https://api.example.com', token: 'test-token' }) await expect(client.init()).resolves.not.toThrow() }) }) // ─── helpers ───────────────────────────────────────────────────────────────── async function initClient() { mockLoadConfig.mockResolvedValue({ endpoint: 'https://api.example.com', token: 'test-token' }) await client.init() } const jsonHeaders = { get: (name: string) => name.toLowerCase() === 'content-type' ? 'application/json' : null } function mockOk(data: unknown) { mockFetch.mockResolvedValue({ ok: true, headers: jsonHeaders, json: () => Promise.resolve(data) }) } function mockFail(status: number, error: string) { mockFetch.mockResolvedValue({ ok: false, status, statusText: error, headers: jsonHeaders, json: () => Promise.resolve({ error }), }) } // ─── object-defs ───────────────────────────────────────────────────────────── describe('listObjectDefs', () => { it('returns object_defs array', async () => { await initClient() mockOk({ object_defs: [{ name: 'user' }, { name: 'task' }] }) const result = await client.listObjectDefs() expect(result).toEqual([{ name: 'user' }, { name: 'task' }]) expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/object-defs') }) }) describe('createObjectDef', () => { it('POSTs to /object-defs with name', async () => { await initClient() mockOk({ name: 'user', created_at: 1234 }) const result = await client.createObjectDef('user') expect(result.name).toBe('user') const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.example.com/object-defs') expect(opts.method).toBe('POST') expect(JSON.parse(opts.body as string)).toEqual({ name: 'user' }) }) }) // ─── objects ───────────────────────────────────────────────────────────────── describe('createObject', () => { it('POSTs to /objects with type only (no custom id)', async () => { await initClient() mockOk({ id: 1, type: 'user', created_at: 1234 }) const result = await client.createObject('user') expect(result.id).toBe(1) expect(typeof result.id).toBe('number') const body = JSON.parse(mockFetch.mock.calls[0][1].body as string) expect(body).toEqual({ type: 'user' }) }) }) describe('getObject', () => { it('GETs /objects/:id with numeric id', async () => { await initClient() mockOk({ id: 42, type: 'user', created_at: 1234 }) const result = await client.getObject(42) expect(result.id).toBe(42) expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/objects/42') }) }) describe('listObjects', () => { it('GETs /objects without filter', async () => { await initClient() mockOk({ objects: [{ id: 1, type: 'user', created_at: 1234 }] }) const result = await client.listObjects() expect(result).toHaveLength(1) expect(result[0].id).toBe(1) expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/objects') }) it('GETs /objects?type=user with filter', async () => { await initClient() mockOk({ objects: [] }) await client.listObjects('user') expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/objects?type=user') }) }) // ─── event-defs ────────────────────────────────────────────────────────────── describe('listEventDefs', () => { it('returns event_defs array', async () => { await initClient() mockOk({ event_defs: [{ name: 'UserCreated', schema: { properties: {} } }] }) const result = await client.listEventDefs() expect(result[0].name).toBe('UserCreated') expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/event-defs') }) }) describe('createEventDef', () => { it('POSTs to /event-defs with name and schema', async () => { await initClient() const schema = { properties: { user: { type: 'ref' as const } } } mockOk({ name: 'UserCreated', schema, hash: 'abc123' }) const result = await client.createEventDef('UserCreated', schema) expect(result.name).toBe('UserCreated') const body = JSON.parse(mockFetch.mock.calls[0][1].body as string) expect(body).toEqual({ name: 'UserCreated', schema }) }) }) // ─── events ────────────────────────────────────────────────────────────────── describe('emitEvent', () => { it('POSTs to /events and returns {event, reactions_fired}', async () => { await initClient() mockOk({ event: { id: 1, type_hash: 'abc123', payload: { user: 1 }, created_at: 1234 }, reactions_fired: 0 }) const result = await client.emitEvent('UserCreated', { user: 1 }) expect(result.event.id).toBe(1) expect(typeof result.event.id).toBe('number') expect(result.event.type_hash).toBe('abc123') expect(result.reactions_fired).toBe(0) const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.example.com/events') expect(opts.method).toBe('POST') const body = JSON.parse(opts.body as string) expect(body).toEqual({ type: 'UserCreated', payload: { user: 1 } }) }) }) describe('getEvent', () => { it('GETs /events/:id with numeric id', async () => { await initClient() mockOk({ id: 5, type_hash: 'abc123', payload: {}, created_at: 1234 }) const result = await client.getEvent(5) expect(result.id).toBe(5) expect(typeof result.id).toBe('number') expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/events/5') }) }) describe('findEventsByRef', () => { it('GETs /events?ref=', async () => { await initClient() mockOk({ events: [{ id: 1, type_hash: 'abc123', payload: {}, created_at: 1234 }] }) const result = await client.findEventsByRef(7) expect(result).toHaveLength(1) expect(result[0].id).toBe(1) expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/events?ref=7') }) }) // ─── projection-defs ───────────────────────────────────────────────────────── describe('listProjectionDefs', () => { it('returns projection_defs array', async () => { await initClient() mockOk({ projection_defs: [ { name: 'userCount', sources: [{ event_def: 'UserCreated', bindings: {}, expression: '$count + 1' }], value_schema: { type: 'number' }, initial_value: 0, }, ], }) const result = await client.listProjectionDefs() expect(result[0].name).toBe('userCount') }) }) describe('createProjectionDef', () => { it('POSTs to /projection-defs with name, sources, params, value_schema, initial_value', async () => { await initClient() const sources = [{ event_def: 'UserCreated', bindings: {}, expression: '$count + 1' }] const params = {} const value_schema = { type: 'number' } const initial_value = 0 mockOk({ name: 'userCount', sources, params, value_schema, initial_value }) const result = await client.createProjectionDef('userCount', sources, params, value_schema, initial_value) expect(result.name).toBe('userCount') const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.example.com/projection-defs') expect(opts.method).toBe('POST') const body = JSON.parse(opts.body as string) expect(body.name).toBe('userCount') expect(body.sources).toEqual(sources) expect(body.value_schema).toEqual(value_schema) expect(body.initial_value).toBe(0) }) }) // ─── projections ───────────────────────────────────────────────────────────── describe('getProjection', () => { it('GETs /projections/:name', async () => { await initClient() mockOk({ value: 42 }) const value = await client.getProjection('userCount') expect(value).toBe(42) expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/projections/userCount') }) it('appends params as query string', async () => { await initClient() mockOk({ value: 5 }) await client.getProjection('tasksByUser', { userId: 'user_01ABC' }) expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/projections/tasksByUser?userId=user_01ABC') }) }) // ─── reactions ─────────────────────────────────────────────────────────────── describe('createReaction (webhook)', () => { it('POSTs to /reactions with action=webhook', async () => { await initClient() mockOk({ id: 1, projection_def_hash: 'hashABC', params_hash: 'paramHash', params: {}, action: 'webhook', webhook_url: 'https://example.com/hook', created_at: 1234, }) const result = await client.createReaction( 'userCount', {}, { action: 'webhook', webhook_url: 'https://example.com/hook' }, ) expect(result.id).toBe(1) expect(typeof result.id).toBe('number') expect(result.action).toBe('webhook') expect(result.webhook_url).toBe('https://example.com/hook') const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.example.com/reactions') expect(opts.method).toBe('POST') const body = JSON.parse(opts.body as string) expect(body.projection_def).toBe('userCount') expect(body.action).toBe('webhook') expect(body.webhook_url).toBe('https://example.com/hook') }) }) describe('createReaction (emit_event)', () => { it('POSTs to /reactions with action=emit_event', async () => { await initClient() mockOk({ id: 2, projection_def_hash: 'hashABC', params_hash: 'paramHash', params: {}, action: 'emit_event', emit_event_type: 'TaskCompleted', created_at: 1234, }) const result = await client.createReaction( 'taskStatus', {}, { action: 'emit_event', emit_event_type: 'TaskCompleted', }, ) expect(result.id).toBe(2) expect(result.action).toBe('emit_event') expect(result.emit_event_type).toBe('TaskCompleted') const body = JSON.parse(mockFetch.mock.calls[0][1].body as string) expect(body.action).toBe('emit_event') expect(body.emit_event_type).toBe('TaskCompleted') }) }) describe('listReactions', () => { it('GETs /reactions', async () => { await initClient() mockOk({ reactions: [] }) const result = await client.listReactions() expect(Array.isArray(result)).toBe(true) expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/reactions') }) }) describe('deleteReaction', () => { it('DELETEs /reactions/:id with numeric id', async () => { await initClient() mockOk({ ok: true }) const result = await client.deleteReaction(3) expect(result.ok).toBe(true) const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.example.com/reactions/3') expect(opts.method).toBe('DELETE') }) }) // ─── health ────────────────────────────────────────────────────────────────── describe('health', () => { it('GETs /health', async () => { await initClient() mockOk({ status: 'ok', version: '2.4.0' }) const result = await client.health() expect(result.status).toBe('ok') expect(result.version).toBe('2.4.0') expect(mockFetch.mock.calls[0][0]).toBe('https://api.example.com/health') }) }) // ─── error handling ────────────────────────────────────────────────────────── describe('error handling', () => { it('throws on 401', async () => { await initClient() mockFail(401, 'Unauthorized') await expect(client.listObjectDefs()).rejects.toThrow('Authentication failed. Check your token.') }) it('throws API error message on non-2xx', async () => { await initClient() mockFail(400, 'Invalid request') await expect(client.createObjectDef('bad')).rejects.toThrow('Invalid request') }) it('throws connection error on fetch failure', async () => { await initClient() mockFetch.mockRejectedValue(new Error('fetch failed: connection refused')) await expect(client.health()).rejects.toThrow('Cannot reach OGraph API at https://api.example.com') }) }) })