Files
lingniu-vehicle-ingest/go/vehicle-gateway/internal/feichibridge/client_test.go

155 lines
5.1 KiB
Go

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)
}
}