d3ro-voice/packages/api-client/__tests__/transcribe.test.ts
2026-08-29 18:33:45 +09:00

82 lines
3.1 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { transcribeAudio } from '../src/transcribe'
const session = {
supabaseUrl: 'https://project.supabase.co',
anonKey: 'anon-key',
token: 'user-token',
}
function payload(transcript = '안녕하세요') {
return {
transcript,
confidence: 0.99,
language_code: 'ko',
duration_seconds: 2.5,
provider: 'groq',
}
}
describe('transcribeAudio', () => {
beforeEach(() => vi.unstubAllGlobals())
it('uses only the authenticated Supabase quota gateway', async () => {
const fetchMock = vi.fn(async () => ({ ok: true, status: 200, json: async () => payload() }))
vi.stubGlobal('fetch', fetchMock)
const result = await transcribeAudio({
...session,
audio: new Blob(['audio'], { type: 'audio/webm' }),
language: 'ko',
})
const [url, options] = fetchMock.mock.calls[0]
expect(url).toBe('https://project.supabase.co/functions/v1/stt-proxy')
expect(options.headers).toEqual({ Authorization: 'Bearer user-token', apikey: 'anon-key' })
expect(options.body).toBeInstanceOf(FormData)
expect((options.body as FormData).get('language_code')).toBe('ko')
expect(result).toEqual({
text: '안녕하세요',
confidence: 0.99,
language: 'ko',
durationSeconds: 2.5,
provider: 'groq',
latencyMs: expect.any(Number),
})
})
it('converts binary and base64 audio to multipart without a JSON bypass', async () => {
const fetchMock = vi.fn(async () => ({ ok: true, status: 200, json: async () => payload('binary') }))
vi.stubGlobal('fetch', fetchMock)
await transcribeAudio({ ...session, audio: Uint8Array.from([0, 1, 2, 253, 254, 255]) })
await transcribeAudio({ ...session, audio: 'AAEC/f7/' })
expect(fetchMock).toHaveBeenCalledTimes(2)
for (const [, options] of fetchMock.mock.calls) {
expect(options.body).toBeInstanceOf(FormData)
expect((options.body as FormData).get('audio')).toBeInstanceOf(Blob)
}
})
it('requires token, anon key and a safe gateway origin before network access', async () => {
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
await expect(transcribeAudio({ ...session, token: '', audio: new Blob(['x']) }))
.rejects.toThrow('authenticated Supabase session')
await expect(transcribeAudio({ ...session, supabaseUrl: 'http://public.example.com', audio: new Blob(['x']) }))
.rejects.toThrow('configuration is invalid')
expect(fetchMock).not.toHaveBeenCalled()
})
it('does not expose upstream bodies and rejects malformed success payloads', async () => {
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: false, status: 503, text: async () => 'secret upstream body' })))
await expect(transcribeAudio({ ...session, audio: new Blob(['x']) }))
.rejects.toThrow('STT gateway request failed (503).')
vi.stubGlobal('fetch', vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ transcript: 'x' }) })))
await expect(transcribeAudio({ ...session, audio: new Blob(['x']) }))
.rejects.toThrow('invalid response')
})
})