// deploy command tests — mock CF API import { describe, it, expect, beforeEach, vi } from 'vitest' import { CloudflareClient } from '../src/cloudflare.js' // ─── Mock fetch ──────────────────────────────────────────────────────────────── const mockFetch = vi.fn() global.fetch = mockFetch function cfOk(result: T) { return { ok: true, json: () => Promise.resolve({ success: true, result, errors: [], messages: [] }) } } function cfFail(code: number, message: string) { return { ok: false, json: () => Promise.resolve({ success: false, result: null, errors: [{ code, message }], messages: [] }), } } // ─── CloudflareClient Tests ──────────────────────────────────────────────────── describe('CloudflareClient', () => { let client: CloudflareClient beforeEach(() => { client = new CloudflareClient('test-cf-token') vi.clearAllMocks() }) // ─── verifyToken ─────────────────────────────────────────────────────────── describe('verifyToken', () => { it('should verify a valid token', async () => { mockFetch.mockResolvedValue(cfOk({ id: 'token-123', status: 'active' })) const result = await client.verifyToken() expect(result.status).toBe('active') expect(result.id).toBe('token-123') const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.cloudflare.com/client/v4/user/tokens/verify') expect(opts.headers).toBeDefined() const headers = opts.headers as Headers expect(headers.get('Authorization')).toBe('Bearer test-cf-token') }) it('should throw on invalid token', async () => { mockFetch.mockResolvedValue(cfFail(1000, 'Invalid API Token')) await expect(client.verifyToken()).rejects.toThrow('Cloudflare API error: [1000] Invalid API Token') }) }) // ─── listAccounts ────────────────────────────────────────────────────────── describe('listAccounts', () => { it('should return accounts list', async () => { mockFetch.mockResolvedValue( cfOk([ { id: 'acct-1', name: 'My Account' }, { id: 'acct-2', name: 'Other Account' }, ]), ) const accounts = await client.listAccounts() expect(accounts).toHaveLength(2) expect(accounts[0]!.id).toBe('acct-1') expect(accounts[1]!.name).toBe('Other Account') expect(mockFetch.mock.calls[0]![0]).toBe('https://api.cloudflare.com/client/v4/accounts') }) }) // ─── createD1Database ────────────────────────────────────────────────────── describe('createD1Database', () => { it('should create a D1 database', async () => { mockFetch.mockResolvedValue(cfOk({ uuid: 'db-uuid-123', name: 'ograph' })) const db = await client.createD1Database('acct-1', 'ograph') expect(db.uuid).toBe('db-uuid-123') expect(db.name).toBe('ograph') const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.cloudflare.com/client/v4/accounts/acct-1/d1/database') expect(opts.method).toBe('POST') expect(JSON.parse(opts.body as string)).toEqual({ name: 'ograph' }) }) it('should throw on permission error', async () => { mockFetch.mockResolvedValue(cfFail(10000, 'Authentication error')) await expect(client.createD1Database('acct-1', 'ograph')).rejects.toThrow( 'Cloudflare API error: [10000] Authentication error', ) }) }) // ─── queryD1 ─────────────────────────────────────────────────────────────── describe('queryD1', () => { it('should execute SQL against D1', async () => { mockFetch.mockResolvedValue(cfOk([{ success: true, results: [], meta: { changes: 3 } }])) const results = await client.queryD1('acct-1', 'db-uuid-123', 'CREATE TABLE test (id INTEGER);') expect(results).toHaveLength(1) expect(results[0]!.success).toBe(true) const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.cloudflare.com/client/v4/accounts/acct-1/d1/database/db-uuid-123/query') expect(opts.method).toBe('POST') expect(JSON.parse(opts.body as string)).toEqual({ sql: 'CREATE TABLE test (id INTEGER);' }) }) }) // ─── uploadWorker ────────────────────────────────────────────────────────── describe('uploadWorker', () => { it('should upload a worker script with bindings', async () => { mockFetch.mockResolvedValue(cfOk({ id: 'ograph', etag: 'abc123' })) const result = await client.uploadWorker('acct-1', 'ograph', 'export default { fetch() {} }', { d1DatabaseId: 'db-uuid-123', d1BindingName: 'DB', apiToken: 'og_testtoken', version: '2.4.0', }) expect(result.id).toBe('ograph') const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.cloudflare.com/client/v4/accounts/acct-1/workers/scripts/ograph') expect(opts.method).toBe('PUT') // Verify multipart body contains metadata and script const body = opts.body as string expect(body).toContain('worker.js') expect(body).toContain('export default { fetch() {} }') expect(body).toContain('d1') expect(body).toContain('db-uuid-123') expect(body).toContain('og_testtoken') expect(body).toContain('2.4.0') // Verify content-type is multipart const headers = opts.headers as Headers expect(headers.get('Content-Type')).toContain('multipart/form-data') }) it('should throw on upload failure', async () => { mockFetch.mockResolvedValue({ ok: false, json: () => Promise.resolve({ success: false, result: null, errors: [{ code: 10007, message: 'Script too large' }], messages: [], }), }) await expect( client.uploadWorker('acct-1', 'ograph', 'code', { d1DatabaseId: 'db-uuid-123', d1BindingName: 'DB', apiToken: 'og_test', version: '2.4.0', }), ).rejects.toThrow('Worker upload failed: [10007] Script too large') }) }) // ─── getWorkersSubdomain ─────────────────────────────────────────────────── describe('getWorkersSubdomain', () => { it('should return subdomain', async () => { mockFetch.mockResolvedValue(cfOk({ subdomain: 'my-workers' })) const subdomain = await client.getWorkersSubdomain('acct-1') expect(subdomain).toBe('my-workers') }) it('should return null on error', async () => { mockFetch.mockResolvedValue(cfFail(10000, 'Not found')) const subdomain = await client.getWorkersSubdomain('acct-1') expect(subdomain).toBeNull() }) }) // ─── setWorkerCustomDomain ───────────────────────────────────────────────── describe('setWorkerCustomDomain', () => { it('should set custom domain', async () => { mockFetch.mockResolvedValue(cfOk({ id: 'domain-1', hostname: 'api.example.com' })) const result = await client.setWorkerCustomDomain('acct-1', 'ograph', 'api.example.com') expect(result.hostname).toBe('api.example.com') const [url, opts] = mockFetch.mock.calls[0] as [string, RequestInit] expect(url).toBe('https://api.cloudflare.com/client/v4/accounts/acct-1/workers/domains') expect(opts.method).toBe('PUT') const body = JSON.parse(opts.body as string) expect(body.hostname).toBe('api.example.com') expect(body.service).toBe('ograph') }) }) }) // ─── Migration Loading Tests ───────────────────────────────────────────────── describe('loadMigrations (integration)', () => { it('should find migration files in the ograph package', async () => { const { readdir, readFile } = await import('node:fs/promises') const { join } = await import('node:path') const migrationsDir = join(import.meta.dirname ?? '.', '..', '..', 'engine', 'migrations') const files = await readdir(migrationsDir) const sqlFiles = files.filter((f) => f.endsWith('.sql')).sort() // Should have migration files expect(sqlFiles.length).toBeGreaterThan(0) // First file should be 0006_v2.sql expect(sqlFiles[0]).toBe('0006_v2.sql') // Last file should be the latest migration expect(sqlFiles[sqlFiles.length - 1]).toBe('0022_projection_health.sql') // Each file should contain SQL for (const file of sqlFiles) { const content = await readFile(join(migrationsDir, file), 'utf-8') expect(content.length).toBeGreaterThan(0) } }) it('should sort files in correct order', async () => { const { readdir } = await import('node:fs/promises') const { join } = await import('node:path') const migrationsDir = join(import.meta.dirname ?? '.', '..', '..', 'engine', 'migrations') const files = await readdir(migrationsDir) const sqlFiles = files.filter((f) => f.endsWith('.sql')).sort() // Verify ordering by number prefix for (let i = 1; i < sqlFiles.length; i++) { const prevNum = parseInt(sqlFiles[i - 1]!.split('_')[0]!, 10) const currNum = parseInt(sqlFiles[i]!.split('_')[0]!, 10) expect(currNum).toBeGreaterThan(prevNum) } }) }) // ─── Token Generation Tests ────────────────────────────────────────────────── describe('generateApiToken', () => { it('should generate tokens with og_ prefix', async () => { const { randomBytes } = await import('node:crypto') const token = `og_${randomBytes(32).toString('hex')}` expect(token).toMatch(/^og_[a-f0-9]{64}$/) }) }) // ─── Deploy Command Registration ──────────────────────────────────────────── describe('createDeployCommand', () => { let createDeployCommand: () => import('commander').Command beforeEach(async () => { const mod = await import('../src/commands/deploy.js') createDeployCommand = mod.createDeployCommand }) it('should create a command named "deploy"', () => { const cmd = createDeployCommand() expect(cmd.name()).toBe('deploy') }) it('should have all expected options', () => { const cmd = createDeployCommand() const optionNames = cmd.options.map((o) => o.long) expect(optionNames).toContain('--cf-token') expect(optionNames).toContain('--account-id') expect(optionNames).toContain('--name') expect(optionNames).toContain('--db-name') expect(optionNames).toContain('--domain') expect(optionNames).toContain('--yes') }) it('should have correct defaults', () => { const cmd = createDeployCommand() const nameOpt = cmd.options.find((o) => o.long === '--name') const dbNameOpt = cmd.options.find((o) => o.long === '--db-name') expect(nameOpt?.defaultValue).toBe('ograph') expect(dbNameOpt?.defaultValue).toBe('ograph') }) it('should show description', () => { const cmd = createDeployCommand() expect(cmd.description()).toContain('Deploy') }) })