package translator import ( "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestLibreTranslateHappyPath(t *testing.T) { t.Parallel() var ( requestSource string requestTarget string requestQ []string requestFormat string ) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) var in requestBody if err := json.Unmarshal(body, &in); err != nil { t.Errorf("unmarshal: %v", err) } requestSource = in.Source requestTarget = in.Target requestQ = in.Q requestFormat = in.Format _ = json.NewEncoder(w).Encode(responseBody{ TranslatedText: []string{"[ru] " + in.Q[0], "[ru] " + in.Q[1]}, }) })) t.Cleanup(server.Close) tr, err := NewLibreTranslate(LibreTranslateConfig{URL: server.URL, Timeout: 2 * time.Second}) if err != nil { t.Fatalf("new: %v", err) } res, err := tr.Translate(context.Background(), "en", "ru", "Hello", "World") if err != nil { t.Fatalf("translate: %v", err) } if res.Engine != LibreTranslateEngine { t.Fatalf("engine = %q, want %q", res.Engine, LibreTranslateEngine) } if res.Subject != "[ru] Hello" || res.Body != "[ru] World" { t.Fatalf("result = %+v", res) } if requestSource != "en" || requestTarget != "ru" || requestFormat != "text" { t.Fatalf("request fields: src=%q dst=%q fmt=%q", requestSource, requestTarget, requestFormat) } if len(requestQ) != 2 || requestQ[0] != "Hello" || requestQ[1] != "World" { t.Fatalf("request q = %v", requestQ) } } func TestLibreTranslateNormalisesLanguageCodes(t *testing.T) { t.Parallel() var src, dst string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) var in requestBody _ = json.Unmarshal(body, &in) src, dst = in.Source, in.Target _ = json.NewEncoder(w).Encode(responseBody{TranslatedText: []string{"a", "b"}}) })) t.Cleanup(server.Close) tr, _ := NewLibreTranslate(LibreTranslateConfig{URL: server.URL}) if _, err := tr.Translate(context.Background(), "EN-US", "ru-RU", "x", "y"); err != nil { t.Fatalf("translate: %v", err) } if src != "en" || dst != "ru" { t.Fatalf("normalised codes src=%q dst=%q, want en/ru", src, dst) } } func TestLibreTranslateUnsupportedPair(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"error":"language not supported"}`)) })) t.Cleanup(server.Close) tr, _ := NewLibreTranslate(LibreTranslateConfig{URL: server.URL}) _, err := tr.Translate(context.Background(), "en", "xx", "subject", "body") if !errors.Is(err, ErrUnsupportedLanguagePair) { t.Fatalf("err = %v, want ErrUnsupportedLanguagePair", err) } } func TestLibreTranslateServerError(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("kaboom")) })) t.Cleanup(server.Close) tr, _ := NewLibreTranslate(LibreTranslateConfig{URL: server.URL}) _, err := tr.Translate(context.Background(), "en", "ru", "subject", "body") if err == nil { t.Fatalf("expected error, got nil") } if errors.Is(err, ErrUnsupportedLanguagePair) { t.Fatalf("err mis-classified as unsupported pair: %v", err) } if !strings.Contains(err.Error(), "500") { t.Fatalf("err = %v, want mention of 500", err) } } func TestLibreTranslateSameSourceAndTargetIsNoop(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Errorf("translator should not call the server for identical src/dst: %s", r.URL.Path) })) t.Cleanup(server.Close) tr, _ := NewLibreTranslate(LibreTranslateConfig{URL: server.URL}) res, err := tr.Translate(context.Background(), "en", "EN", "x", "y") if err != nil { t.Fatalf("translate: %v", err) } if res.Engine != NoopEngine { t.Fatalf("engine = %q, want %q", res.Engine, NoopEngine) } } func TestLibreTranslateRequiresURL(t *testing.T) { t.Parallel() _, err := NewLibreTranslate(LibreTranslateConfig{URL: ""}) if err == nil { t.Fatalf("expected error for empty URL") } } // TestLibreTranslateRejectsMalformedArray defends against a server // that returns a partial / unexpected `translatedText` payload. The // client must surface an error (not panic, not return a half-empty // Result) so the worker can decide between retry and fallback. func TestLibreTranslateRejectsMalformedArray(t *testing.T) { t.Parallel() cases := []struct { name string body string }{ {"single string", `{"translatedText": "only one"}`}, {"array of one", `{"translatedText": ["only one"]}`}, {"empty array", `{"translatedText": []}`}, {"missing field", `{"foo":"bar"}`}, } for _, tc := range cases { body := tc.body t.Run(tc.name, func(t *testing.T) { t.Parallel() server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte(body)) })) t.Cleanup(server.Close) tr, _ := NewLibreTranslate(LibreTranslateConfig{URL: server.URL}) res, err := tr.Translate(context.Background(), "en", "ru", "subject", "body") if err == nil { t.Fatalf("expected error for malformed body %q, got %+v", body, res) } }) } }