fix: add Zod enum validation for challenge types in webhook tester (BUG-S6)
Validates challenge type against the full CHALLENGE_TYPES enum before processing. Invalid types now return 400 instead of silently falling back to speed_blitz. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b82c2755aa
commit
370d8643b7
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { Hono } from 'hono'
|
||||
import { docsRouter } from './docs.js'
|
||||
|
||||
const app = new Hono()
|
||||
app.route('/api/docs', docsRouter)
|
||||
|
||||
describe('docs webhook tester', () => {
|
||||
it('rejects invalid challenge type', async () => {
|
||||
const res = await app.request('/api/docs/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: 'https://example.com/webhook', type: 'invalid_type_xss' }),
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
const json = await res.json() as { error: string }
|
||||
expect(json.error).toContain('Invalid')
|
||||
})
|
||||
|
||||
it('accepts valid challenge type', async () => {
|
||||
// This will fail to actually reach the webhook but should pass validation
|
||||
const res = await app.request('/api/docs/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: 'https://example.com/webhook', type: 'speed_blitz' }),
|
||||
})
|
||||
// Should either succeed or fail at the fetch step — not at validation
|
||||
// Status could be 200 (webhook responded) or 500 (fetch failed)
|
||||
expect(res.status).not.toBe(400)
|
||||
})
|
||||
|
||||
it('rejects missing URL', async () => {
|
||||
const res = await app.request('/api/docs/test', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ type: 'speed_blitz' }),
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
})
|
||||
+16
-11
@@ -1,6 +1,6 @@
|
||||
import { Hono } from 'hono'
|
||||
import { z } from 'zod'
|
||||
import { getAllChallengeTypes } from '../engine/challenges.js'
|
||||
|
||||
export const docsRouter = new Hono()
|
||||
|
||||
docsRouter.get('/webhook', (c) => {
|
||||
@@ -166,23 +166,28 @@ docsRouter.get('/webhook', (c) => {
|
||||
// POST /test — interactive webhook tester (no auth required)
|
||||
docsRouter.post('/test', async (c) => {
|
||||
const body = await c.req.json()
|
||||
const { url, type } = body as { url?: string; type?: string }
|
||||
|
||||
if (!url || typeof url !== 'string') {
|
||||
return c.json({ error: 'Missing "url" field' }, 400)
|
||||
const validTypes = getAllChallengeTypes()
|
||||
const testSchema = z.object({
|
||||
url: z.string().url(),
|
||||
type: z.enum(validTypes as [string, ...string[]]).optional(),
|
||||
})
|
||||
const parsed = testSchema.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return c.json({ error: parsed.error.issues[0]?.message || 'Invalid request' }, 400)
|
||||
}
|
||||
const { url, type } = parsed.data
|
||||
|
||||
// Basic URL validation — must be http(s), no private IPs
|
||||
let parsed: URL
|
||||
// Validate URL protocol and host
|
||||
let parsedUrl: URL
|
||||
try {
|
||||
parsed = new URL(url)
|
||||
parsedUrl = new URL(url)
|
||||
} catch {
|
||||
return c.json({ error: 'Invalid URL' }, 400)
|
||||
}
|
||||
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
|
||||
if (parsedUrl.protocol !== 'https:' && parsedUrl.protocol !== 'http:') {
|
||||
return c.json({ error: 'URL must use http:// or https://' }, 400)
|
||||
}
|
||||
const host = parsed.hostname
|
||||
const host = parsedUrl.hostname
|
||||
if (host === 'localhost' || host === '127.0.0.1' || host.startsWith('192.168.') || host.startsWith('10.') || host.endsWith('.local')) {
|
||||
return c.json({ error: 'Cannot test private/local URLs' }, 400)
|
||||
}
|
||||
@@ -197,7 +202,7 @@ docsRouter.post('/test', async (c) => {
|
||||
}
|
||||
|
||||
const selectedType = (type && challengeTypes[type]) ? type : 'speed_blitz'
|
||||
const ct = challengeTypes[selectedType]
|
||||
const ct = challengeTypes[selectedType] || challengeTypes['speed_blitz']
|
||||
|
||||
const payload = {
|
||||
fight_id: 'test_000000',
|
||||
|
||||
Reference in New Issue
Block a user