import { describe, it, expect, vi, beforeEach } from 'vitest' // Mock DB vi.mock('../db/index.js', () => ({ db: { select: vi.fn().mockReturnValue({ from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ limit: vi.fn().mockResolvedValue([]), }), }), }), insert: vi.fn().mockReturnValue({ values: vi.fn().mockReturnValue({ run: vi.fn() }), }), update: vi.fn().mockReturnValue({ set: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ run: vi.fn() }), }), }), }, schema: { bots: { id: 'id', name: 'name', webhookUrl: 'webhookUrl', eloRating: 'eloRating', isActive: 'isActive', publicKey: 'publicKey', secretHash: 'secretHash' }, fights: { id: 'id', status: 'status', startedAt: 'startedAt' }, rounds: {}, }, sqlite: { transaction: vi.fn((fn: any) => fn()) }, })) // Mock external modules vi.mock('../engine/betting.js', () => ({ lockBets: vi.fn(), settleBets: vi.fn(), })) vi.mock('../engine/payments.js', () => ({ payWinner: vi.fn(), refundEntry: vi.fn(), })) vi.mock('../engine/nostr-publish.js', () => ({ publishFightResult: vi.fn(), })) vi.mock('../engine/queue.js', () => ({ setCooldown: vi.fn(), })) const { isInFight, getActiveFightId, getActiveFighterCount, isMockBot, isAllowedWebhookUrl, } = await import('./orchestrator.js') describe('orchestrator utility functions', () => { it('isInFight returns false for unknown bot', () => { expect(isInFight('unknown-bot-xyz')).toBe(false) }) it('getActiveFightId returns undefined for unknown bot', () => { expect(getActiveFightId('unknown-bot-xyz')).toBeUndefined() }) it('getActiveFighterCount returns a number', () => { expect(typeof getActiveFighterCount()).toBe('number') }) it('isMockBot identifies mock webhook URLs', () => { expect(isMockBot('http://mock.local/bot-1')).toBe(true) expect(isMockBot('http://mock.local')).toBe(true) expect(isMockBot('https://example.com/webhook')).toBe(false) expect(isMockBot('http://human.local/')).toBe(false) }) }) describe('isAllowedWebhookUrl — SSRF protection', () => { it('blocks localhost', () => { expect(isAllowedWebhookUrl('http://localhost/webhook')).toBe(false) expect(isAllowedWebhookUrl('http://127.0.0.1/webhook')).toBe(false) }) it('blocks private IP ranges', () => { expect(isAllowedWebhookUrl('http://10.0.0.1/webhook')).toBe(false) expect(isAllowedWebhookUrl('http://192.168.1.1/webhook')).toBe(false) expect(isAllowedWebhookUrl('http://172.16.0.1/webhook')).toBe(false) }) it('blocks .local and .internal TLDs', () => { expect(isAllowedWebhookUrl('http://myapp.local/webhook')).toBe(false) expect(isAllowedWebhookUrl('http://service.internal/webhook')).toBe(false) }) it('allows public URLs', () => { expect(isAllowedWebhookUrl('https://example.com/webhook')).toBe(true) expect(isAllowedWebhookUrl('https://api.mybot.dev/fight')).toBe(true) }) it('blocks file:// and other schemes', () => { expect(isAllowedWebhookUrl('file:///etc/passwd')).toBe(false) }) // Note: IPv6 loopback (::1) is not currently blocked — tracked for Phase 5 security hardening })