package openplatform import ( "context" "crypto/sha256" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" ) type fakeRepository struct { app AppCredential authErr error vehicles map[string]AuthorizedVehicle hydrogen map[string]DailyHydrogen mileage map[string]DailyMileage priorMileage map[string]DailyMileage authorizedVIN bool totalMileage *TotalMileagePoint audits []string createdHash [sha256.Size]byte createdPrefix string requestedPlates []string dailyVINs []string dailyProtocols []string rangeVINs []string priorVINs []string priorCalls int snapshot MileageSnapshot } func (f *fakeRepository) Authenticate(context.Context, [sha256.Size]byte, time.Time, time.Time, time.Time) (AppCredential, error) { return f.app, f.authErr } func (f *fakeRepository) AuthorizedVehicles(_ context.Context, _ uint64, plates []string, _ time.Time, _ time.Time) (map[string]AuthorizedVehicle, error) { f.requestedPlates = append([]string(nil), plates...) return f.vehicles, nil } func (f *fakeRepository) DailyHydrogen(_ context.Context, vins []string, _ string) (map[string]DailyHydrogen, error) { f.dailyVINs = append([]string(nil), vins...) return f.hydrogen, nil } func (f *fakeRepository) DailyMileage(_ context.Context, vins []string, _ string, protocols []string) (map[string]DailyMileage, error) { f.dailyVINs = append([]string(nil), vins...) f.dailyProtocols = append([]string(nil), protocols...) return f.mileage, nil } func (f *fakeRepository) DailyMileageRange(_ context.Context, vins []string, _, _ string, protocols []string) (map[string]DailyMileage, error) { f.rangeVINs = append([]string(nil), vins...) f.dailyVINs = append([]string(nil), vins...) f.dailyProtocols = append([]string(nil), protocols...) return f.mileage, nil } func (f *fakeRepository) LatestMileageBefore(_ context.Context, vins []string, _ string, protocols []string) (map[string]DailyMileage, error) { f.priorCalls++ f.priorVINs = append([]string(nil), vins...) f.dailyVINs = append([]string(nil), vins...) f.dailyProtocols = append([]string(nil), protocols...) return f.priorMileage, nil } func (f *fakeRepository) CreateMileageSnapshot(_ context.Context, snapshot MileageSnapshot) error { f.snapshot = snapshot return nil } func (f *fakeRepository) LoadMileageSnapshot(_ context.Context, id string, appID uint64, _ time.Time) (MileageSnapshot, error) { if f.snapshot.ID != id || f.snapshot.AppID != appID { return MileageSnapshot{}, ErrInvalidRequest } return f.snapshot, nil } func (f *fakeRepository) AuthorizedVIN(context.Context, uint64, string, time.Time) (bool, error) { return f.authorizedVIN, nil } func (f *fakeRepository) TotalMileage(context.Context, string, time.Time, []string) (*TotalMileagePoint, error) { return f.totalMileage, nil } func (f *fakeRepository) Audit(_ context.Context, _ uint64, endpoint, result, _ string, _ int, _ string) error { f.audits = append(f.audits, endpoint+":"+result) return nil } func (f *fakeRepository) CreateApp(_ context.Context, input AppInput, hash [sha256.Size]byte, prefix string, from time.Time, to *time.Time, actor string) (App, error) { f.createdHash, f.createdPrefix = hash, prefix return App{ID: 1, Name: input.Name, AppKeyPrefix: prefix, Status: input.Status, ValidFrom: from, ValidTo: to, CreatedBy: actor}, nil } func (f *fakeRepository) ListApps(context.Context) ([]App, error) { return nil, nil } func (f *fakeRepository) UpdateApp(context.Context, uint64, AppInput, time.Time, *time.Time, string) (App, error) { return App{}, nil } func (f *fakeRepository) RotateKey(context.Context, uint64, [sha256.Size]byte, string, string) (App, error) { return App{}, nil } func (f *fakeRepository) ReplaceVehicleGrants(context.Context, uint64, []parsedGrant, string) ([]VehicleGrant, error) { return nil, nil } func (f *fakeRepository) ListVehicleGrants(context.Context, uint64) ([]VehicleGrant, error) { return nil, nil } func TestExternalHydrogenAndMileageQueriesPreserveRequestedVehicles(t *testing.T) { repository := &fakeRepository{ app: AppCredential{ID: 7, Name: "partner"}, vehicles: map[string]AuthorizedVehicle{ "粤A12345": {VIN: "LTEST32960VIN0001", Plate: "粤A12345"}, "粤B67890": {VIN: "LTEST32960VIN0002", Plate: "粤B67890"}, }, hydrogen: map[string]DailyHydrogen{ "LTEST32960VIN0001": {VIN: "LTEST32960VIN0001", ConsumptionKg: 12.3154, SampleCount: 1, QualityStatus: "OK"}, }, mileage: map[string]DailyMileage{ "LTEST32960VIN0001": { VIN: "LTEST32960VIN0001", Protocol: "GB32960", MileageKm: 101.235, TotalMileageKm: 12345.679, DataTime: "2026-07-01T23:59:00+08:00", UpdatedAt: "2026-07-02T00:01:00+08:00", }, }, } service := NewService(repository) service.now = func() time.Time { return time.Date(2026, 7, 1, 12, 0, 0, 0, time.FixedZone("CST", 8*3600)) } request := QueryRequest{PlateNumbers: []string{"粤A12345", "粤B67890"}, Date: "2026-07-01"} const key = "0123456789abcdef0123456789abcdef" hydrogen, err := service.QueryHydrogen(context.Background(), key, "trace-h", request) if err != nil { t.Fatal(err) } if len(hydrogen) != 2 || hydrogen[0].HydrogenConsumptionKg == nil || *hydrogen[0].HydrogenConsumptionKg != 12.315 || hydrogen[1].Status != StatusNoData || hydrogen[1].HydrogenConsumptionKg != nil { t.Fatalf("hydrogen = %#v", hydrogen) } mileage, err := service.QueryMileage(context.Background(), key, "trace-m", request) if err != nil { t.Fatal(err) } if len(mileage) != 2 || mileage[0].VIN != "LTEST32960VIN0001" || mileage[0].DailyMileageKm == nil || *mileage[0].DailyMileageKm != 101.235 || mileage[0].TotalMileageKm == nil || *mileage[0].TotalMileageKm != 12345.679 || mileage[0].DataTime == nil || mileage[0].UpdatedAt == nil || mileage[1].Status != StatusNoData || mileage[1].TotalMileageKm != nil || mileage[1].DataTime != nil { t.Fatalf("mileage = %#v", mileage) } } func TestDailyQueriesWithoutPlatesReturnAllAuthorizedVehicles(t *testing.T) { repository := &fakeRepository{ app: AppCredential{ID: 7, Name: "partner"}, vehicles: map[string]AuthorizedVehicle{ "粤B67890": {VIN: "LTEST32960VIN0002", Plate: "粤B67890"}, "粤A12345": {VIN: "LTEST32960VIN0001", Plate: "粤A12345"}, }, hydrogen: map[string]DailyHydrogen{ "LTEST32960VIN0001": {VIN: "LTEST32960VIN0001", ConsumptionKg: 8.5, QualityStatus: "OK"}, }, mileage: map[string]DailyMileage{ "LTEST32960VIN0002": { VIN: "LTEST32960VIN0002", Protocol: "YUTONG_MQTT", MileageKm: 88.8, TotalMileageKm: 10088.8, DataTime: "2026-07-01T20:00:00+08:00", UpdatedAt: "2026-07-01T20:00:01+08:00", }, }, } service := NewService(repository) request := QueryRequest{Date: "2026-07-01"} const key = "0123456789abcdef0123456789abcdef" hydrogen, err := service.QueryHydrogen(context.Background(), key, "trace-h-all", request) if err != nil { t.Fatal(err) } if len(repository.requestedPlates) != 0 || len(hydrogen) != 2 || hydrogen[0].PlateNumber != "粤A12345" || hydrogen[1].PlateNumber != "粤B67890" { t.Fatalf("requested=%v hydrogen=%#v", repository.requestedPlates, hydrogen) } if len(repository.dailyVINs) != 2 || repository.dailyVINs[0] != "LTEST32960VIN0001" || repository.dailyVINs[1] != "LTEST32960VIN0002" { t.Fatalf("daily VINs=%v", repository.dailyVINs) } mileage, err := service.QueryMileage(context.Background(), key, "trace-m-all", QueryRequest{PlateNumbers: []string{}, Date: "2026-07-01"}) if err != nil { t.Fatal(err) } if len(mileage) != 2 || mileage[0].PlateNumber != "粤A12345" || mileage[1].PlateNumber != "粤B67890" || mileage[1].DailyMileageKm == nil { t.Fatalf("mileage=%#v", mileage) } } func TestMileageProtocolPriorityValidationAndExternalSourceName(t *testing.T) { repository := &fakeRepository{ app: AppCredential{ID: 7}, vehicles: map[string]AuthorizedVehicle{ "粤A12345": {VIN: "LTEST32960VIN0001", Plate: "粤A12345"}, }, mileage: map[string]DailyMileage{ "LTEST32960VIN0001": { VIN: "LTEST32960VIN0001", Protocol: "YUTONG_MQTT", MileageKm: 0, TotalMileageKm: 12345, DataTime: "2026-07-01T20:00:00+08:00", UpdatedAt: "2026-07-01T20:00:01+08:00", }, }, } service := NewService(repository) priority := ProtocolPriority{Values: []string{"JT808", "GB32960", "MQTT"}} result, err := service.QueryMileage(context.Background(), "0123456789abcdef0123456789abcdef", "trace-priority", QueryRequest{ PlateNumbers: []string{"粤A12345"}, Date: "2026-07-01", ProtocolPriority: priority, }) if err != nil { t.Fatal(err) } if strings.Join(repository.dailyProtocols, ",") != "JT808,GB32960,YUTONG_MQTT" { t.Fatalf("internal protocols=%v", repository.dailyProtocols) } if len(result) != 1 || result[0].DailyMileageKm == nil || *result[0].DailyMileageKm != 0 || result[0].SourceProtocol == nil || *result[0].SourceProtocol != "MQTT" { t.Fatalf("result=%#v", result) } for _, values := range [][]string{{}, {"YUTONG_MQTT"}, {"GB32960", "GB32960"}, {"mqtt"}} { invalid := ProtocolPriority{Values: values, Present: true} _, err := service.QueryMileage(context.Background(), "0123456789abcdef0123456789abcdef", "trace-invalid", QueryRequest{ Date: "2026-07-01", ProtocolPriority: invalid, }) if !errors.Is(err, ErrInvalidRequest) { t.Fatalf("priority=%v error=%v", values, err) } } } func TestMileageCarriesForwardPreviousTotalAndPreviousCalculationTime(t *testing.T) { previous := DailyMileage{ VIN: "LTEST32960VIN0001", Date: "2026-07-20", Protocol: "YUTONG_MQTT", MileageKm: 35.5, TotalMileageKm: 8888.8, DataTime: "2026-07-20T23:58:00+08:00", UpdatedAt: "2026-07-21T01:15:00+08:00", } repository := &fakeRepository{ app: AppCredential{ID: 7}, vehicles: map[string]AuthorizedVehicle{ "粤A12345": {VIN: "LTEST32960VIN0001", Plate: "粤A12345"}, }, mileage: map[string]DailyMileage{}, priorMileage: map[string]DailyMileage{"LTEST32960VIN0001": previous}, } service := NewService(repository) result, err := service.QueryMileage(context.Background(), "0123456789abcdef0123456789abcdef", "trace-carry", QueryRequest{ PlateNumbers: []string{"粤A12345"}, Date: "2026-07-21", }) if err != nil { t.Fatal(err) } item := result[0] if item.Status != StatusNormal || item.DailyMileageKm == nil || *item.DailyMileageKm != 0 || item.TotalMileageKm == nil || *item.TotalMileageKm != 8888.8 || item.SourceProtocol == nil || *item.SourceProtocol != "MQTT" || item.DataTime == nil || *item.DataTime != previous.DataTime || item.UpdatedAt == nil || *item.UpdatedAt != previous.UpdatedAt { t.Fatalf("carried result=%#v", item) } rangeResult, err := service.QueryMileageRange(context.Background(), "0123456789abcdef0123456789abcdef", "trace-carry-range", MileageRangeRequest{ StartDate: "2026-07-21", EndDate: "2026-07-22", PageSize: 10, }) if err != nil { t.Fatal(err) } if len(rangeResult.Data) != 2 { t.Fatalf("range=%#v", rangeResult) } for _, row := range rangeResult.Data { if row.DailyMileageKm == nil || *row.DailyMileageKm != 0 || row.TotalMileageKm == nil || *row.TotalMileageKm != previous.TotalMileageKm || row.UpdatedAt == nil || *row.UpdatedAt != previous.UpdatedAt { t.Fatalf("carried range row=%#v", row) } } } func TestMileageRangeUsesStableSnapshotAndDistinguishesZeroFromNoData(t *testing.T) { repository := &fakeRepository{ app: AppCredential{ID: 7, Name: "partner"}, vehicles: map[string]AuthorizedVehicle{ "沪A00002": {VIN: "LTEST32960VIN0002", Plate: "沪A00002"}, "沪A00001": {VIN: "LTEST32960VIN0001", Plate: "沪A00001"}, }, mileage: map[string]DailyMileage{ dailyMileageKey("LTEST32960VIN0001", "2026-07-01"): { VIN: "LTEST32960VIN0001", Date: "2026-07-01", Protocol: "GB32960", MileageKm: 0, TotalMileageKm: 12000, DataTime: "2026-07-01T23:58:45+08:00", UpdatedAt: "2026-07-02T05:10:00+08:00", }, dailyMileageKey("LTEST32960VIN0001", "2026-07-02"): { VIN: "LTEST32960VIN0001", Date: "2026-07-02", Protocol: "GB32960", MileageKm: 10.25, TotalMileageKm: 12010.25, DataTime: "2026-07-02T20:00:00+08:00", UpdatedAt: "2026-07-02T20:00:01+08:00", }, dailyMileageKey("LTEST32960VIN0002", "2026-07-02"): { VIN: "LTEST32960VIN0002", Date: "2026-07-02", Protocol: "JT808", MileageKm: 8.5, TotalMileageKm: 9008.5, DataTime: "2026-07-02T21:00:00+08:00", UpdatedAt: "2026-07-02T21:00:01+08:00", }, }, } service := NewService(repository) service.now = func() time.Time { return time.Date(2026, 7, 23, 12, 0, 0, 0, time.FixedZone("CST", 8*3600)) } request := MileageRangeRequest{StartDate: "2026-07-01", EndDate: "2026-07-02", PageSize: 3} const key = "0123456789abcdef0123456789abcdef" first, err := service.QueryMileageRange(context.Background(), key, "trace-range-1", request) if err != nil { t.Fatal(err) } if len(first.Data) != 3 || first.SnapshotID == "" || first.NextCursor == nil { t.Fatalf("first=%#v", first) } if first.Data[0].PlateNumber != "沪A00001" || first.Data[0].DailyMileageKm == nil || *first.Data[0].DailyMileageKm != 0 || first.Data[0].Status != StatusNormal { t.Fatalf("zero mileage row=%#v", first.Data[0]) } if first.Data[1].PlateNumber != "沪A00002" || first.Data[1].Status != StatusNoData || first.Data[1].DailyMileageKm != nil || first.Data[1].DataTime != nil { t.Fatalf("no-data row=%#v", first.Data[1]) } if first.Data[2].Date != "2026-07-02" || first.Data[2].PlateNumber != "沪A00001" { t.Fatalf("third row=%#v", first.Data[2]) } repository.vehicles = map[string]AuthorizedVehicle{} request.Cursor = *first.NextCursor changedPriority := ProtocolPriority{Values: []string{"JT808"}} changedRequest := request changedRequest.ProtocolPriority = changedPriority if _, err := service.QueryMileageRange(context.Background(), key, "trace-range-changed", changedRequest); !errors.Is(err, ErrInvalidRequest) { t.Fatalf("changed protocol priority must invalidate cursor: %v", err) } second, err := service.QueryMileageRange(context.Background(), key, "trace-range-2", request) if err != nil { t.Fatal(err) } if second.SnapshotID != first.SnapshotID || second.NextCursor != nil || len(second.Data) != 1 || second.Data[0].PlateNumber != "沪A00002" || second.Data[0].Date != "2026-07-02" { t.Fatalf("second=%#v", second) } } func TestMileageRangeOnlyLoadsPriorMileageForVINsMissingAtTheirFirstPageDate(t *testing.T) { const ( firstVIN = "LTEST32960VIN0001" secondVIN = "LTEST32960VIN0002" ) repository := &fakeRepository{ app: AppCredential{ID: 7}, vehicles: map[string]AuthorizedVehicle{ "沪A00001": {VIN: firstVIN, Plate: "沪A00001"}, "沪A00002": {VIN: secondVIN, Plate: "沪A00002"}, }, mileage: map[string]DailyMileage{ dailyMileageKey(firstVIN, "2026-07-27"): { VIN: firstVIN, Date: "2026-07-27", Protocol: "GB32960", MileageKm: 12.5, TotalMileageKm: 12012.5, DataTime: "2026-07-27T12:00:00+08:00", UpdatedAt: "2026-07-27T12:00:01+08:00", }, }, priorMileage: map[string]DailyMileage{ secondVIN: { VIN: secondVIN, Date: "2026-07-26", Protocol: "JT808", MileageKm: 8, TotalMileageKm: 9008, DataTime: "2026-07-26T23:00:00+08:00", UpdatedAt: "2026-07-27T01:00:00+08:00", }, }, } result, err := NewService(repository).QueryMileageRange( context.Background(), "0123456789abcdef0123456789abcdef", "trace-range-missing-only", MileageRangeRequest{StartDate: "2026-07-27", EndDate: "2026-07-27", PageSize: 10}, ) if err != nil { t.Fatal(err) } if repository.priorCalls != 1 || len(repository.priorVINs) != 1 || repository.priorVINs[0] != secondVIN { t.Fatalf("prior calls=%d vins=%v, want only %s", repository.priorCalls, repository.priorVINs, secondVIN) } if len(result.Data) != 2 || result.Data[0].DailyMileageKm == nil || *result.Data[0].DailyMileageKm != 12.5 || result.Data[1].DailyMileageKm == nil || *result.Data[1].DailyMileageKm != 0 { t.Fatalf("result=%#v", result) } } func TestMileageRangeSkipsPriorMileageLookupWhenEveryVINHasInitialData(t *testing.T) { const vin = "LTEST32960VIN0001" repository := &fakeRepository{ app: AppCredential{ID: 7}, vehicles: map[string]AuthorizedVehicle{ "沪A00001": {VIN: vin, Plate: "沪A00001"}, }, mileage: map[string]DailyMileage{ dailyMileageKey(vin, "2026-07-27"): { VIN: vin, Date: "2026-07-27", Protocol: "GB32960", MileageKm: 12.5, TotalMileageKm: 12012.5, DataTime: "2026-07-27T12:00:00+08:00", UpdatedAt: "2026-07-27T12:00:01+08:00", }, }, } if _, err := NewService(repository).QueryMileageRange( context.Background(), "0123456789abcdef0123456789abcdef", "trace-range-no-prior", MileageRangeRequest{StartDate: "2026-07-27", EndDate: "2026-07-27", PageSize: 10}, ); err != nil { t.Fatal(err) } if repository.priorCalls != 0 || len(repository.priorVINs) != 0 { t.Fatalf("prior calls=%d vins=%v, want no lookup", repository.priorCalls, repository.priorVINs) } } func TestMileageRangeValidationLimitsWindowAndPageSize(t *testing.T) { service := NewService(&fakeRepository{}) for _, request := range []MileageRangeRequest{ {StartDate: "2025-07-01", EndDate: "2026-07-02", PageSize: 100}, {StartDate: "2026-07-02", EndDate: "2026-07-01", PageSize: 100}, {StartDate: "2026-07-01", EndDate: "2026-07-02", PageSize: 5001}, } { if _, _, _, _, _, _, _, err := service.validateMileageRange(request); !errors.Is(err, ErrInvalidRequest) { t.Fatalf("request=%#v error=%v", request, err) } } } func TestTotalMileageReturnsRecordTimeDifferenceAndProtocolMeaning(t *testing.T) { location := time.FixedZone("CST", 8*3600) repository := &fakeRepository{ app: AppCredential{ID: 7}, authorizedVIN: true, totalMileage: &TotalMileagePoint{VIN: "LA9GG68L2PBAF4790", Protocol: "GB32960", ObservedAt: time.Date(2026, 7, 21, 9, 29, 45, 0, location), TotalMileageKm: 12345.6784}, } service := NewService(repository) service.now = func() time.Time { return time.Date(2026, 7, 21, 12, 0, 0, 0, location) } result, err := service.QueryTotalMileage(context.Background(), "0123456789abcdef0123456789abcdef", "trace-total", TotalMileageQueryRequest{VIN: "LA9GG68L2PBAF4790", Time: "2026-07-21 09:30:00"}) if err != nil { t.Fatal(err) } if result.TotalMileageKm == nil || *result.TotalMileageKm != 12345.678 || result.Protocol != "GB32960" || result.RecordTime != "2026-07-21 09:29:45" || result.TimeDifferenceSeconds == nil || *result.TimeDifferenceSeconds != 15 || result.Status != StatusNormal || !strings.Contains(result.MileageMeaning, "仪表盘") { t.Fatalf("result = %#v", result) } } func TestTotalMileageCanonicalProtocolsAndStrictTime(t *testing.T) { service := NewService(&fakeRepository{}) for _, input := range []string{"GB32960", "YUTONG_MQTT", "JT808"} { _, _, protocols, _, err := service.validateTotalMileageQuery(TotalMileageQueryRequest{VIN: "LA9GG68L2PBAF4790", Time: "2026-07-21 09:30:00", Protocol: input}) if err != nil || len(protocols) != 1 || protocols[0] != input { t.Fatalf("%s: protocols=%v err=%v", input, protocols, err) } } for _, alias := range []string{"32960", "mqtt", "808"} { if _, _, _, _, err := service.validateTotalMileageQuery(TotalMileageQueryRequest{VIN: "LA9GG68L2PBAF4790", Time: "2026-07-21 09:30:00", Protocol: alias}); !errors.Is(err, ErrInvalidRequest) { t.Fatalf("alias %s should be rejected: %v", alias, err) } } if _, _, _, _, err := service.validateTotalMileageQuery(TotalMileageQueryRequest{VIN: "LA9GG68L2PBAF4790", Time: "2026/07/21 09:30"}); !errors.Is(err, ErrInvalidRequest) { t.Fatalf("error=%v", err) } } func TestQueryFailsClosedWhenAnyVehicleIsNotGranted(t *testing.T) { repository := &fakeRepository{ app: AppCredential{ID: 7}, vehicles: map[string]AuthorizedVehicle{"粤A12345": {VIN: "LTEST32960VIN0001", Plate: "粤A12345"}}, } _, err := NewService(repository).QueryHydrogen(context.Background(), "0123456789abcdef0123456789abcdef", "trace", QueryRequest{ PlateNumbers: []string{"粤A12345", "粤B67890"}, Date: "2026-07-01", }) if !errors.Is(err, ErrForbidden) { t.Fatalf("error = %v", err) } } func TestCreateAppReturns32CharacterKeyOnlyOnce(t *testing.T) { repository := &fakeRepository{} service := NewService(repository) created, err := service.CreateApp(context.Background(), AppInput{ Name: "partner", ValidFrom: "2026-07-01T00:00:00+08:00", ValidTo: "2027-07-01T00:00:00+08:00", }, "admin") if err != nil { t.Fatal(err) } if !appKeyPattern.MatchString(created.AppKey) || len(created.AppKey) != 32 || created.AppKeyPrefix != created.AppKey[:8] { t.Fatalf("created = %#v", created) } if created.AppKey[12] != '4' || !strings.ContainsRune("89ab", rune(created.AppKey[16])) { t.Fatalf("appKey is not UUID v4: %s", created.AppKey) } if repository.createdHash != sha256.Sum256([]byte(created.AppKey)) || repository.createdPrefix != created.AppKey[:8] { t.Fatal("repository did not receive the appKey hash and prefix") } } func TestExternalHandlerUsesDocumentEnvelopeAndErrors(t *testing.T) { repository := &fakeRepository{app: AppCredential{ID: 1}, vehicles: map[string]AuthorizedVehicle{ "粤A12345": {VIN: "LTEST32960VIN0001", Plate: "粤A12345"}, }, hydrogen: map[string]DailyHydrogen{}} handler := NewHandler(NewService(repository)) request := httptest.NewRequest(http.MethodPost, HydrogenQueryPath, strings.NewReader(`{"plateNumbers":["粤A12345"],"date":"2026-07-01"}`)) request.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef") response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("status=%d body=%s", response.Code, response.Body.String()) } var body ExternalResponse if err := json.Unmarshal(response.Body.Bytes(), &body); err != nil { t.Fatal(err) } if body.Code != "SUCCESS" || body.Message != "success" || len(body.TraceID) != 32 { t.Fatalf("body = %#v", body) } allRequest := httptest.NewRequest(http.MethodPost, MileageQueryPath, strings.NewReader(`{"date":"2026-07-01"}`)) allRequest.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef") allResponse := httptest.NewRecorder() handler.ServeHTTP(allResponse, allRequest) if allResponse.Code != http.StatusOK || !strings.Contains(allResponse.Body.String(), `"plateNumber":"粤A12345"`) { t.Fatalf("status=%d body=%s", allResponse.Code, allResponse.Body.String()) } rangeRequest := httptest.NewRequest(http.MethodPost, MileageRangeQueryPath, strings.NewReader(`{"startDate":"2026-07-01","endDate":"2026-07-01","pageSize":10}`)) rangeRequest.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef") rangeResponse := httptest.NewRecorder() handler.ServeHTTP(rangeResponse, rangeRequest) if rangeResponse.Code != http.StatusOK || !strings.Contains(rangeResponse.Body.String(), `"snapshotId"`) || !strings.Contains(rangeResponse.Body.String(), `"nextCursor":null`) { t.Fatalf("status=%d body=%s", rangeResponse.Code, rangeResponse.Body.String()) } badDate := httptest.NewRequest(http.MethodPost, HydrogenQueryPath, strings.NewReader(`{"plateNumbers":["粤A12345"],"date":"2026/07/01"}`)) badDate.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef") badResponse := httptest.NewRecorder() handler.ServeHTTP(badResponse, badDate) if badResponse.Code != http.StatusBadRequest || !strings.Contains(badResponse.Body.String(), `"code":"INVALID_DATE_FORMAT"`) { t.Fatalf("status=%d body=%s", badResponse.Code, badResponse.Body.String()) } badProtocol := httptest.NewRequest(http.MethodPost, MileageQueryPath, strings.NewReader(`{"date":"2026-07-01","protocolPriority":["GB32960","GB32960"]}`)) badProtocol.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef") badProtocolResponse := httptest.NewRecorder() handler.ServeHTTP(badProtocolResponse, badProtocol) if badProtocolResponse.Code != http.StatusBadRequest || !strings.Contains(badProtocolResponse.Body.String(), `"message":"protocolPriority不能包含重复协议"`) || !strings.Contains(badProtocolResponse.Body.String(), `"traceId":"`) { t.Fatalf("status=%d body=%s", badProtocolResponse.Code, badProtocolResponse.Body.String()) } nullProtocol := httptest.NewRequest(http.MethodPost, MileageQueryPath, strings.NewReader(`{"date":"2026-07-01","protocolPriority":null}`)) nullProtocol.Header.Set("Authorization", "Bearer 0123456789abcdef0123456789abcdef") nullProtocolResponse := httptest.NewRecorder() handler.ServeHTTP(nullProtocolResponse, nullProtocol) if nullProtocolResponse.Code != http.StatusBadRequest || !strings.Contains(nullProtocolResponse.Body.String(), `"message":"protocolPriority不能为空"`) { t.Fatalf("status=%d body=%s", nullProtocolResponse.Code, nullProtocolResponse.Body.String()) } }