package stt import ( "context" "errors" "io" "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" "github.com/kami/maven/internal/audio" ) func TestHTTPTranscriberSendsRawPCM(t *testing.T) { t.Parallel() var gotBody []byte var gotHeader http.Header srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotBody, _ = io.ReadAll(r.Body) gotHeader = r.Header.Clone() w.Header().Set("Content-Type", "application/json") _, _ = io.WriteString(w, `{"text":"привет","confidence":0.82}`) })) defer srv.Close() a := audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("pcm-bytes")} tr := NewHTTPTranscriber(srv.URL, "s3cret", "ru", 2*time.Second) text, conf, err := tr.Transcribe(context.Background(), a) if err != nil { t.Fatalf("Transcribe: %v", err) } if text != "привет" || conf != 0.82 { t.Fatalf("got %q %v", text, conf) } if string(gotBody) != "pcm-bytes" { t.Fatalf("body should be the PCM itself, got %q", gotBody) } if got := gotHeader.Get("X-Sample-Rate"); got != strconv.Itoa(audio.PCM16kMono.SampleRate) { t.Fatalf("X-Sample-Rate = %q", got) } if got := gotHeader.Get("X-Language"); got != "ru" { t.Fatalf("X-Language = %q", got) } // Audio is the most sensitive thing crossing this seam. if got := gotHeader.Get("Authorization"); got != "Bearer s3cret" { t.Fatalf("Authorization = %q", got) } } func TestHTTPTranscriberOmitsEmptyToken(t *testing.T) { t.Parallel() var auth string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { auth = r.Header.Get("Authorization") _, _ = io.WriteString(w, `{"text":"x","confidence":0}`) })) defer srv.Close() a := audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("x")} if _, _, err := NewHTTPTranscriber(srv.URL, "", "ru", time.Second).Transcribe(context.Background(), a); err != nil { t.Fatalf("Transcribe: %v", err) } if auth != "" { t.Fatalf("Authorization should be absent, got %q", auth) } } func TestHTTPTranscriberRejectsInvalidSuccessResponse(t *testing.T) { t.Parallel() tests := []struct { name string body string }{ {name: "empty object", body: `{}`}, {name: "blank text", body: `{"text":" ","confidence":0.5}`}, {name: "missing confidence", body: `{"text":"hello"}`}, {name: "negative confidence", body: `{"text":"hello","confidence":-0.1}`}, {name: "confidence above one", body: `{"text":"hello","confidence":1.1}`}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, tc.body) })) defer srv.Close() _, _, err := NewHTTPTranscriber(srv.URL, "", "ru", time.Second). Transcribe(context.Background(), audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("x")}) if !errors.Is(err, ErrInvalidTranscript) { t.Fatalf("error = %v, want ErrInvalidTranscript", err) } }) } } func TestHTTPTranscriberRejectsOversizeResponse(t *testing.T) { t.Parallel() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = io.WriteString(w, strings.Repeat("x", int(MaxTranscriptResponseBytes+1))) })) defer srv.Close() _, _, err := NewHTTPTranscriber(srv.URL, "", "ru", time.Second). Transcribe(context.Background(), audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("x")}) if !errors.Is(err, ErrInvalidTranscript) { t.Fatalf("error = %v, want ErrInvalidTranscript", err) } } func TestHTTPTranscriberRefusesWrongFormat(t *testing.T) { t.Parallel() a := audio.Audio{Format: audio.Format{SampleRate: 44100, Channels: 2, SampleBits: 16, Encoding: "pcm_s16le"}} _, _, err := NewHTTPTranscriber("http://example.invalid", "", "ru", time.Second).Transcribe(context.Background(), a) if !errors.Is(err, ErrFormat) { t.Fatalf("want ErrFormat, got %v", err) } } func TestHTTPTranscriberErrorsOnBadStatus(t *testing.T) { t.Parallel() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusUnauthorized) })) defer srv.Close() a := audio.Audio{Format: audio.PCM16kMono, Bytes: []byte("x")} _, _, err := NewHTTPTranscriber(srv.URL, "", "ru", time.Second).Transcribe(context.Background(), a) if err == nil { t.Fatal("a 401 must be an error, so the Pair falls back") } }