d3ro-voice/server/supabase/functions/stt-proxy/index.contract.test.ts
2026-08-29 18:33:45 +09:00

51 lines
2.8 KiB
TypeScript

const source = await Deno.readTextFile(new URL('./index.ts', import.meta.url))
function assert(condition: boolean, message: string): asserts condition {
if (!condition) throw new Error(message)
}
Deno.test('STT proxy has no synthetic success fallback', () => {
for (const forbidden of [
'd3ro-cloud-mock',
'음성 전사 완료',
'D3RO Cloud STT',
]) {
assert(!source.includes(forbidden), `forbidden synthetic fallback remains: ${forbidden}`)
}
assert(source.includes("const error = status === 503 ? 'stt_provider_unavailable' : 'stt_upstream_failed'"),
'provider failures must produce an explicit 502/503 error')
})
Deno.test('gateway uses a dedicated D3RO API token', () => {
assert(source.includes("Deno.env.get('D3RO_API_TOKEN')"), 'dedicated backend token is required')
assert(source.includes('if (apiServerUrl && apiServerToken)'), 'gateway must be skipped without its own token')
assert(!source.includes("req.headers.get('Authorization')"), 'Supabase user JWT must not be forwarded to D3RO API')
assert(source.includes('createInternalSttGatewayUrl(apiServerUrl)'),
'quota-owning internal endpoint must use the canonical URL guard')
assert(source.includes("'X-D3RO-STT-Gateway-Token': apiServerToken"),
'dedicated token must use the internal gateway header')
assert(!source.includes('headers: { Authorization: authorization }'),
'dedicated gateway token must not be accepted as a user JWT')
})
Deno.test('atomic quota is reserved before provider work and finalized before success', () => {
const reserveIndex = source.indexOf('const quota = await reserveSttQuota(')
const denialIndex = source.indexOf('if (!quota.allowed', reserveIndex)
const providerIndex = source.indexOf("const apiServerUrl = Deno.env.get('D3RO_API_URL')", denialIndex)
const finalizeIndex = source.indexOf('await finalizeSttQuota(quotaReservation.id, true', providerIndex)
const successIndex = source.indexOf('return new Response(JSON.stringify(result)', finalizeIndex)
assert(reserveIndex >= 0, 'atomic quota reservation is missing')
assert(denialIndex > reserveIndex, 'quota denial is not checked')
assert(providerIndex > denialIndex, 'provider work begins before quota denial')
assert(finalizeIndex > providerIndex, 'successful provider work does not finalize quota')
assert(successIndex > finalizeIndex, 'transcript success precedes quota finalization')
assert(source.includes('finalizeSttQuota(quotaReservation.id, false'),
'failed provider work does not release quota')
})
Deno.test('unexpected errors are sanitized', () => {
assert(source.includes("JSON.stringify({ error: 'internal_error' })"), 'generic internal error response is missing')
assert(!source.includes('JSON.stringify({ error: message })'), 'raw exception messages must not be returned')
})