test: add orchestrator test suite — utility functions and SSRF protection (9 tests)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a96e8922b6
commit
e6c3894443
@@ -0,0 +1,105 @@
|
|||||||
|
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
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user