Skip to content

Commit de300d1

Browse files
committed
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.
1 parent 1b88e42 commit de300d1

2 files changed

Lines changed: 174 additions & 0 deletions

File tree

.github/workflows/e2e.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: E2E (DeepL compatibility)
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
# Daily canary: catches the day DeepL changes its anti-bot scheme and
7+
# DeepLX starts 429-ing, before users pile in with issues.
8+
- cron: "17 2 * * *"
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
e2e:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-go@v5
19+
with:
20+
go-version-file: go.mod
21+
cache: true
22+
- name: Run live DeepL E2E tests
23+
run: go test -tags=e2e -v -timeout=10m ./translate/...

translate/translate_e2e_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//go:build e2e
2+
3+
// End-to-end tests that exercise the real DeepL upstream through
4+
// TranslateByDeepLX. Gated behind the `e2e` build tag so the default
5+
// `go test ./...` (and existing CI) never hit the network or risk
6+
// tripping DeepL's rate limiting. Run explicitly:
7+
//
8+
// go test -tags=e2e -v ./translate/...
9+
//
10+
// A 429 means THIS IP got rate-limited, not that the code is broken —
11+
// those cases back off and ultimately t.Skip rather than t.Fail, so the
12+
// suite stays a signal for "DeepLX can still reach DeepL", not a flaky
13+
// red build.
14+
package translate_test
15+
16+
import (
17+
"net/http"
18+
"strings"
19+
"testing"
20+
"time"
21+
22+
"github.com/OwO-Network/DeepLX/translate"
23+
)
24+
25+
const (
26+
e2ePace = 2 * time.Second
27+
e2eBackoff = 3 * time.Second
28+
e2eMaxRetries = 3
29+
)
30+
31+
// translateFree paces the call and backs off on 429. A persistent 429
32+
// skips the test (upstream/IP problem); anything else is returned to
33+
// assert on.
34+
func translateFree(t *testing.T, source, target, text string) translate.DeepLXTranslationResult {
35+
t.Helper()
36+
var res translate.DeepLXTranslationResult
37+
for attempt := 1; attempt <= e2eMaxRetries; attempt++ {
38+
time.Sleep(e2ePace)
39+
var err error
40+
res, err = translate.TranslateByDeepLX(source, target, text, "", "", "")
41+
if err != nil {
42+
t.Fatalf("TranslateByDeepLX returned a transport error: %v", err)
43+
}
44+
if res.Code == http.StatusTooManyRequests {
45+
wait := time.Duration(attempt) * e2eBackoff
46+
t.Logf("attempt %d/%d: DeepL returned 429 (this IP is rate-limited); backing off %s", attempt, e2eMaxRetries, wait)
47+
time.Sleep(wait)
48+
continue
49+
}
50+
return res
51+
}
52+
t.Skipf("DeepL kept returning 429 after %d attempts — upstream rate limit / IP block, not a code defect", e2eMaxRetries)
53+
return res
54+
}
55+
56+
func TestE2EFreeBasic(t *testing.T) {
57+
res := translateFree(t, "EN", "ZH", "Hello, world")
58+
if res.Code != http.StatusOK {
59+
t.Fatalf("expected 200, got %d: %s", res.Code, res.Message)
60+
}
61+
if res.Data == "" {
62+
t.Fatal("expected a non-empty translation, got empty Data")
63+
}
64+
if res.Method != "Free" {
65+
t.Errorf("expected Method=Free, got %q", res.Method)
66+
}
67+
t.Logf("EN->ZH %q => %q", "Hello, world", res.Data)
68+
}
69+
70+
func TestE2EFreeAutoDetect(t *testing.T) {
71+
// Empty source => server-side language autodetection.
72+
res := translateFree(t, "", "EN", "Bonjour le monde")
73+
if res.Code != http.StatusOK {
74+
t.Fatalf("expected 200, got %d: %s", res.Code, res.Message)
75+
}
76+
if res.Data == "" {
77+
t.Fatal("expected a non-empty translation, got empty Data")
78+
}
79+
if res.SourceLang == "" {
80+
t.Error("expected a detected SourceLang, got empty")
81+
}
82+
t.Logf("auto->EN %q => %q (detected %s)", "Bonjour le monde", res.Data, res.SourceLang)
83+
}
84+
85+
func TestE2EFreeJapanese(t *testing.T) {
86+
res := translateFree(t, "EN", "JA", "Good morning")
87+
if res.Code != http.StatusOK {
88+
t.Fatalf("expected 200, got %d: %s", res.Code, res.Message)
89+
}
90+
if res.Data == "" {
91+
t.Fatal("expected a non-empty translation, got empty Data")
92+
}
93+
}
94+
95+
// TestE2ENoRateLimitOnNormalUse is the core of "make sure it doesn't 429":
96+
// a handful of properly-paced requests should essentially all succeed. If
97+
// every one is blocked we skip (IP problem); otherwise any 429 on paced
98+
// traffic means the browser impersonation is degrading and needs a look.
99+
func TestE2ENoRateLimitOnNormalUse(t *testing.T) {
100+
phrases := []string{"one", "two", "three", "four", "five"}
101+
var ok, rateLimited int
102+
for i, p := range phrases {
103+
time.Sleep(e2ePace)
104+
res, err := translate.TranslateByDeepLX("EN", "ZH", p, "", "", "")
105+
if err != nil {
106+
t.Fatalf("round %d: unexpected transport error: %v", i, err)
107+
}
108+
switch res.Code {
109+
case http.StatusOK:
110+
ok++
111+
case http.StatusTooManyRequests:
112+
rateLimited++
113+
default:
114+
t.Errorf("round %d: unexpected code %d: %s", i, res.Code, res.Message)
115+
}
116+
}
117+
t.Logf("normal-use rounds=%d ok=%d rateLimited=%d", len(phrases), ok, rateLimited)
118+
if ok == 0 {
119+
t.Skipf("all %d requests were rate-limited — upstream/IP block, not a code defect", len(phrases))
120+
}
121+
if rateLimited > 0 {
122+
t.Errorf("got %d/%d 429s on properly-paced requests; DeepL impersonation may be degrading", rateLimited, len(phrases))
123+
}
124+
}
125+
126+
// TestE2EInputValidation never reaches the network — these inputs are
127+
// rejected before any DeepL call, so it is safe and free even under -tags=e2e.
128+
func TestE2EInputValidation(t *testing.T) {
129+
cases := []struct {
130+
name string
131+
source, target string
132+
text string
133+
wantCode int
134+
}{
135+
{"empty text", "EN", "ZH", "", http.StatusNotFound},
136+
{"target auto", "EN", "auto", "hello", http.StatusBadRequest},
137+
{"unsupported target", "EN", "XX", "hello", http.StatusBadRequest},
138+
{"too long", "EN", "ZH", strings.Repeat("a", 1501), http.StatusRequestEntityTooLarge},
139+
}
140+
for _, tc := range cases {
141+
t.Run(tc.name, func(t *testing.T) {
142+
res, err := translate.TranslateByDeepLX(tc.source, tc.target, tc.text, "", "", "")
143+
if err != nil {
144+
t.Fatalf("unexpected error: %v", err)
145+
}
146+
if res.Code != tc.wantCode {
147+
t.Errorf("want code %d, got %d: %s", tc.wantCode, res.Code, res.Message)
148+
}
149+
})
150+
}
151+
}

0 commit comments

Comments
 (0)