package syb import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" ) // `[必须]` 本文件全部用 httptest 起假 OCR 服务端,绝不能打真实的 // ocr.ilapage.cn——见工单 #47 和 admin/AGENTS.md。 func ocrBody(code int, message, data string) []byte { b, _ := json.Marshal(map[string]any{"code": code, "message": message, "data": data}) return b } // ── 三种约定的假响应(工单 #47「怎么验证」逐条要求) ────────────── func TestOcrClient_Recognize_识别成功(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(ocrBody(200, "Success", "kycv")) })) defer srv.Close() ocr, err := NewOcrClient(srv.URL, time.Second) if err != nil { t.Fatalf("创建 OCR 客户端失败: %v", err) } data, err := ocr.Recognize(context.Background(), []byte("fake-image")) if err != nil { t.Fatalf("识别应该成功,实际报错: %v", err) } if data != "kycv" { t.Errorf("识别结果应该是 kycv,实际 %q", data) } } func TestOcrClient_Recognize_code200但data为空不是错误(t *testing.T) { // `[必须]` 实测:无文字图片探测得到 code:200 + data:"",这不是 // HTTP/JSON 错误,Recognize 应该返回 (空字符串, nil),由调用方 // (LoginWithOCR)的长度校验去判定"这次没识别出来"。 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(ocrBody(200, "Success", "")) })) defer srv.Close() ocr, err := NewOcrClient(srv.URL, time.Second) if err != nil { t.Fatalf("创建 OCR 客户端失败: %v", err) } data, err := ocr.Recognize(context.Background(), []byte("fake-image")) if err != nil { t.Fatalf("code:200 + data:\"\" 不应该被当成错误,实际报错: %v", err) } if data != "" { t.Errorf("期望空字符串,实际 %q", data) } } func TestOcrClient_Recognize_长度不对时原样返回交给调用方校验(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(ocrBody(200, "Success", "ab")) })) defer srv.Close() ocr, err := NewOcrClient(srv.URL, time.Second) if err != nil { t.Fatalf("创建 OCR 客户端失败: %v", err) } data, err := ocr.Recognize(context.Background(), []byte("fake-image")) if err != nil { t.Fatalf("HTTP 调用本身没问题,不应该报错,实际: %v", err) } if data != "ab" { t.Errorf("Recognize 不做长度校验,应该原样返回,实际 %q", data) } } // ── 请求内容:字段名 file,multipart 上传,不落盘 ────────────── func TestOcrClient_Recognize_用multipart字段名file上传(t *testing.T) { var gotFieldName string var gotBytes []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(1 << 20); err != nil { t.Errorf("解析 multipart 表单失败: %v", err) } if r.MultipartForm != nil && len(r.MultipartForm.File) > 0 { for name, files := range r.MultipartForm.File { gotFieldName = name if len(files) > 0 { f, _ := files[0].Open() gotBytes = make([]byte, files[0].Size) f.Read(gotBytes) f.Close() } } } w.Write(ocrBody(200, "Success", "abcd")) })) defer srv.Close() ocr, _ := NewOcrClient(srv.URL, time.Second) image := []byte("这是验证码图片的字节内容") if _, err := ocr.Recognize(context.Background(), image); err != nil { t.Fatalf("识别失败: %v", err) } if gotFieldName != "file" { t.Errorf("multipart 字段名应该是 file,实际 %q", gotFieldName) } if string(gotBytes) != string(image) { t.Errorf("上传的图片字节应该原样传到服务端,实际 %q", string(gotBytes)) } } // ── 降级路径:不可达 / 非 JSON / 非 200 ────────────────────── func TestOcrClient_Recognize_服务不可达返回error(t *testing.T) { // 关掉的服务器:地址存在但连不上。 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) srv.Close() ocr, err := NewOcrClient(srv.URL, 500*time.Millisecond) if err != nil { t.Fatalf("创建 OCR 客户端失败: %v", err) } _, err = ocr.Recognize(context.Background(), []byte("fake-image")) if err == nil { t.Fatal("服务连不上应该返回 error") } } func TestOcrClient_Recognize_非JSON响应返回error不panic(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("不是 JSON")) })) defer srv.Close() ocr, _ := NewOcrClient(srv.URL, time.Second) _, err := ocr.Recognize(context.Background(), []byte("fake-image")) if err == nil { t.Fatal("非 JSON 响应应该返回 error") } } func TestOcrClient_Recognize_HTTP非200返回error(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte("internal error")) })) defer srv.Close() ocr, _ := NewOcrClient(srv.URL, time.Second) _, err := ocr.Recognize(context.Background(), []byte("fake-image")) if err == nil { t.Fatal("HTTP 500 应该返回 error") } } func TestOcrClient_Recognize_业务code非200返回error(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(ocrBody(500, "内部错误", "")) })) defer srv.Close() ocr, _ := NewOcrClient(srv.URL, time.Second) _, err := ocr.Recognize(context.Background(), []byte("fake-image")) if err == nil { t.Fatal("code != 200 应该返回 error") } } func TestNewOcrClient_地址留空报错(t *testing.T) { if _, err := NewOcrClient("", time.Second); err == nil { t.Fatal("空地址应该报错——调用方应该在这之前就判断 ocr_url 是否留空并跳过 OCR") } if _, err := NewOcrClient(" ", time.Second); err == nil { t.Fatal("空白地址同样应该报错") } } // ── OCR 请求不带顺运宝 Cookie:独立 http.Client ────────────── func TestOcrClient_不携带顺运宝的Cookie(t *testing.T) { var gotCookie string sybSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.SetCookie(w, &http.Cookie{Name: "erp_session", Value: "should-not-leak", Path: "/"}) w.Header().Set("Content-Type", "image/jpeg") w.Write([]byte("fake-jpeg")) })) defer sybSrv.Close() ocrSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ck, err := r.Cookie("erp_session"); err == nil { gotCookie = ck.Value } w.Write(ocrBody(200, "Success", "abcd")) })) defer ocrSrv.Close() sybClient, err := New(sybSrv.URL) if err != nil { t.Fatalf("创建顺运宝客户端失败: %v", err) } captcha, err := sybClient.FetchCaptcha(context.Background()) if err != nil { t.Fatalf("获取验证码失败: %v", err) } ocrClient, err := NewOcrClient(ocrSrv.URL, time.Second) if err != nil { t.Fatalf("创建 OCR 客户端失败: %v", err) } if _, err := ocrClient.Recognize(context.Background(), captcha.Image); err != nil { t.Fatalf("识别失败: %v", err) } if gotCookie != "" { t.Fatalf("OCR 请求不应该带上顺运宝的 Cookie,实际带了 %q", gotCookie) } }