package gzev import ( "context" "encoding/json" "errors" "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" ) func TestDecimalPreservesNullAndZero(t *testing.T) { for _, tc := range []struct { in string valid bool value string }{{"null", false, ""}, {`"0.0"`, true, "0.0"}, {"203.125", true, "203.125"}} { var d Decimal if err := json.Unmarshal([]byte(tc.in), &d); err != nil || d.Valid != tc.valid || d.Value != tc.value { t.Fatalf("%s: %+v %v", tc.in, d, err) } } for _, bad := range []string{`""`, `"NaN"`, `-1`, `"1.2345"`, `"100000000000"`} { var d Decimal if json.Unmarshal([]byte(bad), &d) == nil { t.Fatalf("accepted %s", bad) } } } func TestDateShanghaiBoundary(t *testing.T) { now := time.Date(2026, 9, 27, 16, 0, 0, 0, time.UTC) if ValidateDate("2026-09-27", now) != nil || ValidateDate("2026-09-28", now) == nil { t.Fatal("Shanghai day cutoff incorrect") } } func TestFetchPaginationAndCookie(t *testing.T) { calls := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++ if r.Header.Get("Authorization") != "Bearer fake" { t.Error("missing bearer") } cookie, e := r.Cookie("token") if e != nil || cookie.Value != "fake" { t.Error("missing token cookie") } if r.URL.Query().Get("queryBeginDate") != "2024-01-01" || r.URL.Query().Get("queryEndDate") != "2024-01-01" || r.URL.Query().Get("queryType") != "2" { t.Error("wrong daily query") } fmt.Fprintf(w, `{"code":"0","data":{"pageNum":%d,"total":2,"list":[{"vin":"LNXNEGRR7SR31945%d","dateRange":"2024-01-01 - 2024-01-01","runMileage":"12.1","hyRunMileage":null,"chargeRunMileage":"0"}]}}`, calls, calls) })) defer srv.Close() c := NewClient(Session{Authorization: "Bearer fake", Cookies: []SessionCookie{{"token", "fake"}}}) c.baseURL = srv.URL c.PageSize = 1 c.Delay = 0 day, err := c.FetchDay(context.Background(), "2024-01-01") if err != nil || len(day.Rows) != 2 || len(day.Pages) != 2 || calls != 2 { t.Fatalf("pagination: %+v %v", day, err) } if day.Rows[0].Hydrogen.Valid || !day.Rows[0].Charge.Valid { t.Fatal("null lost") } } func TestRejectsIncompleteSource(t *testing.T) { for name, body := range map[string]string{ "missing code": `{"data":{}}`, "missing total": `{"code":"0","data":{"pageNum":1,"list":[]}}`, "empty page": `{"code":"0","data":{"pageNum":1,"total":2,"list":[]}}`, "wrong date": `{"code":"0","data":{"pageNum":1,"total":1,"list":[{"vin":"LNXNEGRR7SR319456","dateRange":"2024-01-01 - 2024-01-02"}]}}`, } { t.Run(name, func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, body) })) defer srv.Close() c := NewClient(Session{}) c.baseURL = srv.URL if _, err := c.FetchDay(context.Background(), "2024-01-01"); err == nil { t.Fatal("accepted incomplete day") } }) } } func TestAuthAndPermissionAreDifferent(t *testing.T) { for _, tc := range []struct { code string auth bool }{{"401", true}, {"00000004", false}, {"403", false}} { calls := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls++; fmt.Fprintf(w, `{"code":%q}`, tc.code) })) c := NewClient(Session{}) c.baseURL = srv.URL _, err := c.FetchDay(context.Background(), "2024-01-01") srv.Close() if err == nil || errors.Is(err, ErrAuth) != tc.auth || calls != 1 { t.Fatalf("code %s: %v calls=%d", tc.code, err, calls) } } } func TestNoCredentialRedirect(t *testing.T) { hit := false target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hit = true })) defer target.Close() source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target.URL, 302) })) defer source.Close() c := NewClient(Session{Authorization: "Bearer fake"}) c.baseURL = source.URL _, err := c.FetchDay(context.Background(), "2024-01-01") if hit || !errors.Is(err, ErrAuth) { t.Fatalf("redirect leaked credentials: %v", err) } } func TestReadSessionPermissionsAndCookieAllowlist(t *testing.T) { p := filepath.Join(t.TempDir(), "session.json") body := `{"authorization":"Bearer fake","cookies":[{"name":"token","value":"fake"}]}` os.WriteFile(p, []byte(body), 0644) if _, err := ReadSession(p); err == nil { t.Fatal("accepted world-readable session") } os.Chmod(p, 0600) if _, err := ReadSession(p); err != nil { t.Fatal(err) } os.WriteFile(p, []byte(strings.ReplaceAll(body, `"name":"token"`, `"name":"password"`)), 0600) if _, err := ReadSession(p); err == nil { t.Fatal("accepted password cookie") } } func TestVerifyAccountRejectsOtherLogin(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/evdata-sso/api/base/getUserInfo" || r.URL.Query().Get("token") != "fake" { t.Error("incorrect identity contract") } fmt.Fprint(w, `{"code":"0","data":{"loginname":"different-account"}}`) })) defer srv.Close() c := NewClient(Session{Authorization: "Bearer fake"}) c.baseURL = srv.URL if c.VerifyAccount(context.Background(), "guangdonglingniu") == nil { t.Fatal("mixed account data") } }