package provider import ( "context" "encoding/json" "fmt" "net/http" "strings" "testing" ) // TestOpenAISynthese200: 200 mit Bytes → Bytes unverändert; POST // /v1/audio/speech, Bearer-Key, Body mit model/voice/input/response_format // laut Task-Brief (Default-Modell tts-1, Default-Stimme alloy). func TestOpenAISynthese200(t *testing.T) { ts, rec := fakeTTSServer(t, 200, fakeMP3) p := NewOpenAI(ChainConfig{ Getenv: envMap(map[string]string{ EnvOpenAIKey: "sk-test", EnvOpenAIEndpoint: ts.URL, }), }) data, err := p.Synthesize(context.Background(), "Build fertig", "en") if err != nil { t.Fatalf("Synthesize: %v", err) } if string(data) != string(fakeMP3) { t.Errorf("Synthesize = %q, want Fake-Bytes", data) } if rec.method != http.MethodPost { t.Errorf("Methode = %s, want POST", rec.method) } if want := "/v1/audio/speech"; rec.path != want { t.Errorf("Pfad = %q, want %q", rec.path, want) } if got := rec.header.Get("Authorization"); got != "Bearer sk-test" { t.Errorf("Authorization = %q, want Bearer sk-test", got) } var body struct { Model string `json:"model"` Voice string `json:"voice"` Input string `json:"input"` ResponseFormat string `json:"response_format"` } if err := json.Unmarshal(rec.body, &body); err != nil { t.Fatalf("Body ist kein JSON: %v (%s)", err, rec.body) } if body.Model != "tts-1" { t.Errorf("model = %q, want Default tts-1", body.Model) } if body.Voice != "alloy" { t.Errorf("voice = %q, want Default alloy", body.Voice) } if body.Input != "Build fertig" { t.Errorf("input = %q", body.Input) } if body.ResponseFormat != "mp3" { t.Errorf("response_format = %q, want mp3", body.ResponseFormat) } } // TestOpenAIHTTPFehler: 500 und 401 sind klare Fehler mit Status. func TestOpenAIHTTPFehler(t *testing.T) { for _, status := range []int{500, 401} { t.Run(http.StatusText(status), func(t *testing.T) { ts, _ := fakeTTSServer(t, status, []byte("boom")) p := NewOpenAI(ChainConfig{Getenv: envMap(map[string]string{ EnvOpenAIKey: "sk-test", EnvOpenAIEndpoint: ts.URL, })}) _, err := p.Synthesize(context.Background(), "Hallo", "de") if err == nil || !strings.Contains(err.Error(), fmt.Sprintf("%d", status)) { t.Fatalf("Synthesize-Fehler = %v, will HTTP %d", err, status) } if !strings.Contains(err.Error(), "openai") { t.Errorf("Fehler soll den Provider nennen: %v", err) } }) } } // TestOpenAIModelKonfigurierbar: [providers.openai].model ändert den // Modell-String im Body (Entscheidungsnotiz: Default tts-1, Alternativen // wie gpt-4o-mini-tts erlaubt). func TestOpenAIModelKonfigurierbar(t *testing.T) { ts, rec := fakeTTSServer(t, 200, fakeMP3) p := NewOpenAI(ChainConfig{ Getenv: envMap(map[string]string{ EnvOpenAIKey: "sk-test", EnvOpenAIEndpoint: ts.URL, }), Model: "gpt-4o-mini-tts", }) if _, err := p.Synthesize(context.Background(), "Hallo", "de"); err != nil { t.Fatalf("Synthesize: %v", err) } var body struct { Model string `json:"model"` } if err := json.Unmarshal(rec.body, &body); err != nil { t.Fatal(err) } if body.Model != "gpt-4o-mini-tts" { t.Errorf("model = %q, want gpt-4o-mini-tts", body.Model) } } // TestOpenAIAvailable: Key entscheidet — ohne Netz-Call. func TestOpenAIAvailable(t *testing.T) { if NewOpenAI(ChainConfig{Getenv: envMap(nil)}).Available(context.Background()) { t.Error("Available ohne Key = true, want false") } p := NewOpenAI(ChainConfig{Getenv: envMap(map[string]string{EnvOpenAIKey: "sk"})}) if !p.Available(context.Background()) { t.Error("Available mit Key = false, want true") } } // TestOpenAIVoiceRangfolge: Pin schlägt Mapping schlägt Default. func TestOpenAIVoiceRangfolge(t *testing.T) { t.Run("Mapping schlägt Default", func(t *testing.T) { ts, rec := fakeTTSServer(t, 200, fakeMP3) p := NewOpenAI(ChainConfig{ Getenv: envMap(map[string]string{ EnvOpenAIKey: "sk", EnvOpenAIEndpoint: ts.URL, }), Voices: map[string]map[string]string{"openai": {"en": "nova"}}, }) if _, err := p.Synthesize(context.Background(), "Hi", "en"); err != nil { t.Fatalf("Synthesize: %v", err) } var body struct { Voice string `json:"voice"` } json.Unmarshal(rec.body, &body) if body.Voice != "nova" { t.Errorf("voice = %q, want nova (config)", body.Voice) } }) t.Run("Pin schlägt Mapping", func(t *testing.T) { ts, rec := fakeTTSServer(t, 200, fakeMP3) p := NewOpenAI(ChainConfig{ Getenv: envMap(map[string]string{ EnvOpenAIKey: "sk", EnvOpenAIEndpoint: ts.URL, }), Voices: map[string]map[string]string{"openai": {"en": "nova"}}, }) p.PinVoice("echo") if _, err := p.Synthesize(context.Background(), "Hi", "en"); err != nil { t.Fatalf("Synthesize: %v", err) } var body struct { Voice string `json:"voice"` } json.Unmarshal(rec.body, &body) if body.Voice != "echo" { t.Errorf("voice = %q, want echo (--voice)", body.Voice) } }) }