82 lines
3.1 KiB
TypeScript
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')
|
|
})
|
|
})
|