package feichibridge import ( "context" "encoding/base64" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" "time" ) type fixedCaptchaSolver struct { code string calls int } func (s *fixedCaptchaSolver) Solve(_ context.Context, image []byte) (string, error) { s.calls++ if string(image) != "captcha-image" { return "", fmt.Errorf("unexpected captcha image") } return s.code, nil } func TestAPIClientUsesSessionHeadersAndExpectedEndpoints(t *testing.T) { var calls int server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { calls++ if request.Header.Get("Cookie") != "R_SESS=test" || request.Header.Get("x-api-csrf") != "csrf" { t.Errorf("missing session headers: %#v", request.Header) } response.Header().Set("Content-Type", "application/json") switch { case request.URL.Path == "/api/v1/sys/vehicleRealStatuss": _ = json.NewEncoder(response).Encode(map[string]any{ "code": 200, "data": []map[string]string{{"vehicleId": "id-1", "vin": "LTEST32960VIN0001"}}, }) case strings.Contains(request.URL.Path, "vehicleRealStatussByVId"): _ = json.NewEncoder(response).Encode(map[string]any{ "code": 200, "data": map[string]any{"vehicleId": "id-1", "vin": "LTEST32960VIN0001", "dataItems": map[string]string{"2000": "2026-07-17 12:00:00"}}, }) case request.URL.Path == "/api/v1/sys/hisdataQuerys": _ = json.NewEncoder(response).Encode(map[string]any{ "code": 200, "data": []map[string]any{{"subdata": []map[string]string{{"2000": "2026-07-17 11:59:50"}}}}, }) default: http.NotFound(response, request) } })) defer server.Close() client, err := NewAPIClient(server.URL, map[string]string{"Cookie": "R_SESS=test", "x-api-csrf": "csrf"}, time.Second) if err != nil { t.Fatal(err) } vehicles, err := client.Vehicles(context.Background()) if err != nil || len(vehicles) != 1 { t.Fatalf("vehicles = %#v, err = %v", vehicles, err) } if _, err := client.Snapshot(context.Background(), "id-1"); err != nil { t.Fatal(err) } records, err := client.History(context.Background(), vehicles[0].VIN, time.Now().Add(-time.Hour), time.Now()) if err != nil || len(records) != 1 { t.Fatalf("history = %#v, err = %v", records, err) } if calls != 3 { t.Fatalf("calls = %d", calls) } } func TestAPIClientMapsUnauthorizedEnvelope(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, _ *http.Request) { _, _ = response.Write([]byte(`{"code":401,"data":[]}`)) })) defer server.Close() client, err := NewAPIClient(server.URL, nil, time.Second) if err != nil { t.Fatal(err) } if _, err := client.Vehicles(context.Background()); err != ErrUnauthorized { t.Fatalf("error = %v", err) } } func TestAuthenticatedAPIClientLogsInAndRetriesExpiredSession(t *testing.T) { var loginCalls int var vehicleCalls int solver := &fixedCaptchaSolver{code: "12WN"} server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { response.Header().Set("Content-Type", "application/json") switch request.URL.Path { case "/api/v1/first-login": response.Header().Set("x-api-csrf", "csrf-value") response.Header().Add("Set-Cookie", "CSRF=csrf-value; Path=/") _, _ = response.Write([]byte(`{"code":200}`)) case "/api/v1/login/randCode": if request.Header.Get("x-api-csrf") != "csrf-value" { t.Errorf("captcha CSRF = %q", request.Header.Get("x-api-csrf")) } src := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString([]byte("captcha-image")) _ = json.NewEncoder(response).Encode(map[string]any{"code": 200, "data": map[string]string{"src": src}}) case "/api/v1/login": loginCalls++ var payload map[string]string _ = json.NewDecoder(request.Body).Decode(&payload) if payload["username"] != "JXQN0003" || payload["validCode"] != "12WN" { t.Errorf("login payload = %#v", payload) } encrypted, err := base64.StdEncoding.DecodeString(payload["password"]) if err != nil || len(encrypted) != 128 || payload["password"] == "JXQN0003-" { t.Errorf("password was not RSA encrypted: len=%d err=%v", len(encrypted), err) } _ = json.NewEncoder(response).Encode(map[string]any{ "code": 200, "data": map[string]string{"token": fmt.Sprintf("token-%d", loginCalls)}, }) case "/api/v1/sys/vehicleRealStatuss": vehicleCalls++ if vehicleCalls == 1 { _, _ = response.Write([]byte(`{"code":401,"data":[]}`)) return } if request.Header.Get("Cookie") != "CSRF=csrf-value; R_SESS=token-2" { t.Errorf("session cookie = %q", request.Header.Get("Cookie")) } _, _ = response.Write([]byte(`{"code":200,"data":[]}`)) default: http.NotFound(response, request) } })) defer server.Close() client, err := NewAuthenticatedAPIClient( server.URL, LoginCredentials{Username: "JXQN0003", Password: "JXQN0003-", MaxAttempts: 2}, solver, time.Second, ) if err != nil { t.Fatal(err) } if _, err := client.Vehicles(context.Background()); err != nil { t.Fatal(err) } if loginCalls != 2 || solver.calls != 2 || vehicleCalls != 2 { t.Fatalf("login=%d solver=%d vehicles=%d", loginCalls, solver.calls, vehicleCalls) } }