155 lines
5.1 KiB
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)
|
|
}
|
|
}
|