package api import ( "bytes" "context" "encoding/json" "io" "mime/multipart" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "github.com/sandrodach/mindsift/internal/config" ) // fakeWhisper answers like speaches: model list and OpenAI-style transcriptions. func fakeWhisper(t *testing.T, models []string, got *map[string]string, downloads *int32) *httptest.Server { t.Helper() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && r.URL.Path == "/v1/models": var data []map[string]string for _, m := range models { data = append(data, map[string]string{"id": m}) } _ = json.NewEncoder(w).Encode(map[string]interface{}{"data": data}) case r.Method == http.MethodPost && strings.HasPrefix(r.URL.Path, "/v1/models/"): atomic.AddInt32(downloads, 1) w.WriteHeader(http.StatusCreated) case r.Method == http.MethodPost && r.URL.Path == "/v1/audio/transcriptions": if r.Header.Get("Authorization") != "Bearer geheim" { http.Error(w, "unauthorized", http.StatusUnauthorized) return } _ = r.ParseMultipartForm(10 << 20) f, _, _ := r.FormFile("file") audio, _ := io.ReadAll(f) *got = map[string]string{"model": r.FormValue("model"), "language": r.FormValue("language"), "audio": string(audio)} _ = json.NewEncoder(w).Encode(map[string]string{"text": " Hallo Welt, wie geht's? "}) default: http.NotFound(w, r) } })) t.Cleanup(ts.Close) return ts } func voiceUpload(t *testing.T, router http.Handler, token string, audio []byte) *httptest.ResponseRecorder { t.Helper() var body bytes.Buffer mw := multipart.NewWriter(&body) fw, _ := mw.CreateFormFile("file", "aufnahme.webm") _, _ = fw.Write(audio) _ = mw.Close() req := httptest.NewRequest("POST", "/api/v1/voice/transcribe", &body) req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+token) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) return rec } func voiceServer(t *testing.T, speech config.SpeechConfig) (*Server, string) { t.Helper() srv, _, token := setupTestServer(t) srv.cfg.Speech = speech srv.voice = newVoiceService(speech.MaxParallel) return srv, token } func TestVoiceTranscribeForwardsToWhisper(t *testing.T) { var got map[string]string var downloads int32 whisper := fakeWhisper(t, []string{"Systran/faster-whisper-small"}, &got, &downloads) srv, token := voiceServer(t, config.SpeechConfig{URL: whisper.URL + "/v1", Model: "Systran/faster-whisper-small", Language: "de", APIKey: "geheim", MaxSeconds: 600, MaxUploadMB: 1, MaxParallel: 1, PerUserPerHour: 2, TimeoutSeconds: 10}) router := srv.Router() rec := voiceUpload(t, router, token, []byte("RIFF-audio")) if rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"text":"Hallo Welt, wie geht's?"`) { t.Fatalf("transcribe: %d %s", rec.Code, rec.Body.String()) } if got["model"] != "Systran/faster-whisper-small" || got["language"] != "de" || got["audio"] != "RIFF-audio" { t.Fatalf("forwarded wrong data: %+v", got) } // Hourly budget per user _ = voiceUpload(t, router, token, []byte("x")) if rec := voiceUpload(t, router, token, []byte("x")); rec.Code != http.StatusTooManyRequests { t.Fatalf("over the hourly budget: want 429, got %d", rec.Code) } // Model already there: the startup check reports ready and requests no download st := srv.checkVoice(context.Background()) if !st.Reachable || !st.ModelReady || downloads != 0 { t.Fatalf("status %+v, downloads %d", st, downloads) } // Status for the login screen and the admin card, without the API key if body := sessionCall(router, "GET", "/api/v1/auth/status", "", "", "").Body.String(); !strings.Contains(body, `"mode":"server"`) { t.Fatalf("auth status: %s", body) } body := sessionCall(router, "GET", "/api/v1/admin/voice", token, "", "").Body.String() if !strings.Contains(body, `"modelReady":true`) || strings.Contains(body, "geheim") { t.Fatalf("admin voice status: %s", body) } } func TestVoiceUploadLimitAndModes(t *testing.T) { var got map[string]string var downloads int32 whisper := fakeWhisper(t, nil, &got, &downloads) srv, token := voiceServer(t, config.SpeechConfig{URL: whisper.URL, Model: "m", MaxUploadMB: 1, MaxParallel: 1, PerUserPerHour: 10, TimeoutSeconds: 10}) if rec := voiceUpload(t, srv.Router(), token, bytes.Repeat([]byte("x"), 3<<20)); rec.Code != http.StatusRequestEntityTooLarge && rec.Code != http.StatusBadRequest { t.Fatalf("too large: want 413/400, got %d", rec.Code) } if st := srv.checkVoice(context.Background()); st.ModelReady || !st.Reachable { t.Fatalf("missing model must be reported: %+v", st) } if m := (config.SpeechConfig{URL: "http://whisper:8000"}).Mode(true); m != "browser" { t.Fatalf("demo must use the browser, got %s", m) } if m := (config.SpeechConfig{}).Mode(false); m != "browser" { t.Fatalf("without URL: browser, got %s", m) } if m := (config.SpeechConfig{URL: "x", Disabled: true}).Mode(false); m != "off" { t.Fatalf("disabled: off, got %s", m) } browserSrv, browserToken := voiceServer(t, config.SpeechConfig{MaxParallel: 1, PerUserPerHour: 10}) if rec := voiceUpload(t, browserSrv.Router(), browserToken, []byte("x")); rec.Code != http.StatusNotFound { t.Fatalf("browser mode: want 404, got %d", rec.Code) } }