mirror of
https://github.com/OwO-Network/DeepLX.git
synced 2026-07-04 07:49:47 +00:00
test(e2e): add live DeepL compatibility test suite
Gated behind the e2e build tag so default go test and existing CI are unaffected. Covers free-endpoint translation, autodetection, input validation, and a paced no-429 upstream-compatibility canary, plus a scheduled/manual workflow.
This commit is contained in:
parent
1b88e428ae
commit
de300d16b5
23
.github/workflows/e2e.yaml
vendored
Normal file
23
.github/workflows/e2e.yaml
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
name: E2E (DeepL compatibility)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
# Daily canary: catches the day DeepL changes its anti-bot scheme and
|
||||
# DeepLX starts 429-ing, before users pile in with issues.
|
||||
- cron: "17 2 * * *"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- name: Run live DeepL E2E tests
|
||||
run: go test -tags=e2e -v -timeout=10m ./translate/...
|
||||
151
translate/translate_e2e_test.go
Normal file
151
translate/translate_e2e_test.go
Normal file
@ -0,0 +1,151 @@
|
||||
//go:build e2e
|
||||
|
||||
// End-to-end tests that exercise the real DeepL upstream through
|
||||
// TranslateByDeepLX. Gated behind the `e2e` build tag so the default
|
||||
// `go test ./...` (and existing CI) never hit the network or risk
|
||||
// tripping DeepL's rate limiting. Run explicitly:
|
||||
//
|
||||
// go test -tags=e2e -v ./translate/...
|
||||
//
|
||||
// A 429 means THIS IP got rate-limited, not that the code is broken —
|
||||
// those cases back off and ultimately t.Skip rather than t.Fail, so the
|
||||
// suite stays a signal for "DeepLX can still reach DeepL", not a flaky
|
||||
// red build.
|
||||
package translate_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/OwO-Network/DeepLX/translate"
|
||||
)
|
||||
|
||||
const (
|
||||
e2ePace = 2 * time.Second
|
||||
e2eBackoff = 3 * time.Second
|
||||
e2eMaxRetries = 3
|
||||
)
|
||||
|
||||
// translateFree paces the call and backs off on 429. A persistent 429
|
||||
// skips the test (upstream/IP problem); anything else is returned to
|
||||
// assert on.
|
||||
func translateFree(t *testing.T, source, target, text string) translate.DeepLXTranslationResult {
|
||||
t.Helper()
|
||||
var res translate.DeepLXTranslationResult
|
||||
for attempt := 1; attempt <= e2eMaxRetries; attempt++ {
|
||||
time.Sleep(e2ePace)
|
||||
var err error
|
||||
res, err = translate.TranslateByDeepLX(source, target, text, "", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("TranslateByDeepLX returned a transport error: %v", err)
|
||||
}
|
||||
if res.Code == http.StatusTooManyRequests {
|
||||
wait := time.Duration(attempt) * e2eBackoff
|
||||
t.Logf("attempt %d/%d: DeepL returned 429 (this IP is rate-limited); backing off %s", attempt, e2eMaxRetries, wait)
|
||||
time.Sleep(wait)
|
||||
continue
|
||||
}
|
||||
return res
|
||||
}
|
||||
t.Skipf("DeepL kept returning 429 after %d attempts — upstream rate limit / IP block, not a code defect", e2eMaxRetries)
|
||||
return res
|
||||
}
|
||||
|
||||
func TestE2EFreeBasic(t *testing.T) {
|
||||
res := translateFree(t, "EN", "ZH", "Hello, world")
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", res.Code, res.Message)
|
||||
}
|
||||
if res.Data == "" {
|
||||
t.Fatal("expected a non-empty translation, got empty Data")
|
||||
}
|
||||
if res.Method != "Free" {
|
||||
t.Errorf("expected Method=Free, got %q", res.Method)
|
||||
}
|
||||
t.Logf("EN->ZH %q => %q", "Hello, world", res.Data)
|
||||
}
|
||||
|
||||
func TestE2EFreeAutoDetect(t *testing.T) {
|
||||
// Empty source => server-side language autodetection.
|
||||
res := translateFree(t, "", "EN", "Bonjour le monde")
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", res.Code, res.Message)
|
||||
}
|
||||
if res.Data == "" {
|
||||
t.Fatal("expected a non-empty translation, got empty Data")
|
||||
}
|
||||
if res.SourceLang == "" {
|
||||
t.Error("expected a detected SourceLang, got empty")
|
||||
}
|
||||
t.Logf("auto->EN %q => %q (detected %s)", "Bonjour le monde", res.Data, res.SourceLang)
|
||||
}
|
||||
|
||||
func TestE2EFreeJapanese(t *testing.T) {
|
||||
res := translateFree(t, "EN", "JA", "Good morning")
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d: %s", res.Code, res.Message)
|
||||
}
|
||||
if res.Data == "" {
|
||||
t.Fatal("expected a non-empty translation, got empty Data")
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2ENoRateLimitOnNormalUse is the core of "make sure it doesn't 429":
|
||||
// a handful of properly-paced requests should essentially all succeed. If
|
||||
// every one is blocked we skip (IP problem); otherwise any 429 on paced
|
||||
// traffic means the browser impersonation is degrading and needs a look.
|
||||
func TestE2ENoRateLimitOnNormalUse(t *testing.T) {
|
||||
phrases := []string{"one", "two", "three", "four", "five"}
|
||||
var ok, rateLimited int
|
||||
for i, p := range phrases {
|
||||
time.Sleep(e2ePace)
|
||||
res, err := translate.TranslateByDeepLX("EN", "ZH", p, "", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("round %d: unexpected transport error: %v", i, err)
|
||||
}
|
||||
switch res.Code {
|
||||
case http.StatusOK:
|
||||
ok++
|
||||
case http.StatusTooManyRequests:
|
||||
rateLimited++
|
||||
default:
|
||||
t.Errorf("round %d: unexpected code %d: %s", i, res.Code, res.Message)
|
||||
}
|
||||
}
|
||||
t.Logf("normal-use rounds=%d ok=%d rateLimited=%d", len(phrases), ok, rateLimited)
|
||||
if ok == 0 {
|
||||
t.Skipf("all %d requests were rate-limited — upstream/IP block, not a code defect", len(phrases))
|
||||
}
|
||||
if rateLimited > 0 {
|
||||
t.Errorf("got %d/%d 429s on properly-paced requests; DeepL impersonation may be degrading", rateLimited, len(phrases))
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2EInputValidation never reaches the network — these inputs are
|
||||
// rejected before any DeepL call, so it is safe and free even under -tags=e2e.
|
||||
func TestE2EInputValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
source, target string
|
||||
text string
|
||||
wantCode int
|
||||
}{
|
||||
{"empty text", "EN", "ZH", "", http.StatusNotFound},
|
||||
{"target auto", "EN", "auto", "hello", http.StatusBadRequest},
|
||||
{"unsupported target", "EN", "XX", "hello", http.StatusBadRequest},
|
||||
{"too long", "EN", "ZH", strings.Repeat("a", 1501), http.StatusRequestEntityTooLarge},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res, err := translate.TranslateByDeepLX(tc.source, tc.target, tc.text, "", "", "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if res.Code != tc.wantCode {
|
||||
t.Errorf("want code %d, got %d: %s", tc.wantCode, res.Code, res.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user