package provider import ( "context" "encoding/json" "fmt" "net/http" "strings" "testing" ) // TestOpenAISynthesize200: 200 with bytes → bytes unchanged; POST // /v1/audio/speech, bearer key, body with model/voice/input/response_format // per the API contract (default model tts-1, default voice alloy). func TestOpenAISynthesize200(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 finished", "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("method = %s, want POST", rec.method) } if want := "/v1/audio/speech"; rec.path != want { t.Errorf("path = %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 is not 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 finished" { t.Errorf("input = %q", body.Input) } if body.ResponseFormat != "mp3" { t.Errorf("response_format = %q, want mp3", body.ResponseFormat) } } // TestOpenAIHTTPError: 500 and 401 are clear errors with the status. func TestOpenAIHTTPError(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(), "Hello", "de") if err == nil || !strings.Contains(err.Error(), fmt.Sprintf("%d", status)) { t.Fatalf("Synthesize error = %v, want HTTP %d", err, status) } if !strings.Contains(err.Error(), "openai") { t.Errorf("error should name the provider: %v", err) } }) } } // TestOpenAIModelConfigurable: [providers.openai].model changes the model // string in the body (decision note: default tts-1, alternatives such as // gpt-4o-mini-tts allowed). func TestOpenAIModelConfigurable(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(), "Hello", "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: the key decides; without a network call. func TestOpenAIAvailable(t *testing.T) { if NewOpenAI(ChainConfig{Getenv: envMap(nil)}).Available(context.Background()) { t.Error("Available without key = true, want false") } p := NewOpenAI(ChainConfig{Getenv: envMap(map[string]string{EnvOpenAIKey: "sk"})}) if !p.Available(context.Background()) { t.Error("Available with key = false, want true") } } // TestOpenAIVoicePrecedence: pin beats mapping beats default. func TestOpenAIVoicePrecedence(t *testing.T) { t.Run("mapping beats 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 beats 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) } }) }