Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates. Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
813 lines
37 KiB
Go
813 lines
37 KiB
Go
package openplatform
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func float64Pointer(value float64) *float64 { return &value }
|
|
|
|
type fakeRepository struct {
|
|
app AppCredential
|
|
authErr error
|
|
vehicles map[string]AuthorizedVehicle
|
|
hydrogen map[string]DailyHydrogen
|
|
mileage map[string]DailyMileage
|
|
priorMileage map[string]DailyMileage
|
|
rollbacks map[string]bool
|
|
authorizedVIN bool
|
|
totalMileage *TotalMileagePoint
|
|
realtime map[string]RealtimeVehiclePoint
|
|
stationary []StationaryLocationPoint
|
|
stations []HydrogenStation
|
|
audits []string
|
|
createdHash [sha256.Size]byte
|
|
createdPrefix string
|
|
requestedPlates []string
|
|
dailyVINs []string
|
|
dailyProtocols []string
|
|
rangeVINs []string
|
|
priorVINs []string
|
|
priorCalls int
|
|
snapshot MileageSnapshot
|
|
stationaryRadius float64
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Repository stub retains fixture values; reconciliation itself is tested with
|
|
// actual dated observations in mileage_reconciliation_test.go.
|
|
func (f *fakeRepository) ReconciledMileageRange(ctx context.Context, vins []string, start, end string, protocols []string) (map[string]DailyMileage, error) {
|
|
f.dailyVINs = append([]string(nil), vins...)
|
|
f.rangeVINs = append([]string(nil), vins...)
|
|
f.dailyProtocols = protocols
|
|
out := map[string]DailyMileage{}
|
|
carried := map[string]DailyMileage{}
|
|
var missing []string
|
|
for _, vin := range vins {
|
|
v, ok := f.mileage[dailyMileageKey(vin, start)]
|
|
if !ok {
|
|
v, ok = f.mileage[vin]
|
|
}
|
|
if (!ok || !validDailyMileage(v)) && !f.rollbacks[dailyMileageKey(vin, start)] {
|
|
missing = append(missing, vin)
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
prior, _ := f.LatestMileageBefore(ctx, missing, start, protocols)
|
|
for vin, v := range prior {
|
|
carried[vin] = v
|
|
}
|
|
}
|
|
first, _ := time.Parse("2006-01-02", start)
|
|
last, _ := time.Parse("2006-01-02", end)
|
|
for day := first; !day.After(last); day = day.AddDate(0, 0, 1) {
|
|
date := day.Format("2006-01-02")
|
|
for _, vin := range vins {
|
|
key := dailyMileageKey(vin, date)
|
|
v, ok := f.mileage[key]
|
|
if !ok {
|
|
v, ok = f.mileage[vin]
|
|
}
|
|
if ok && validDailyMileage(v) {
|
|
out[key] = v
|
|
carried[vin] = v
|
|
} else if f.rollbacks[key] {
|
|
out[key] = DailyMileage{DataQuality: mileageTotalRollbackQuality}
|
|
delete(carried, vin)
|
|
} else if v, ok = carried[vin]; ok && validDailyMileage(v) {
|
|
v.MileageKm = 0
|
|
out[key] = v
|
|
}
|
|
}
|
|
}
|
|
return out, 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) MileageRollbacks(context.Context, []string, string, string, []string) (map[string]bool, error) {
|
|
return f.rollbacks, 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) StationaryLocationPoints(_ context.Context, _ []string, _ time.Time, _ time.Time, _ float64, _ float64, radiusMeters float64, _ float64) ([]StationaryLocationPoint, error) {
|
|
f.stationaryRadius = radiusMeters
|
|
return f.stationary, nil
|
|
}
|
|
func (f *fakeRepository) RealtimeVehicles(context.Context, []string, time.Time) (map[string]RealtimeVehiclePoint, error) {
|
|
return f.realtime, nil
|
|
}
|
|
func (f *fakeRepository) HydrogenStations(context.Context, HydrogenStationRequest) ([]HydrogenStation, error) {
|
|
return f.stations, nil
|
|
}
|
|
func (f *fakeRepository) Audit(_ context.Context, _ uint64, endpoint, result, _ string, _ int, _ string) error {
|
|
f.audits = append(f.audits, endpoint+":"+result)
|
|
return nil
|
|
}
|
|
|
|
func TestRealtimeVehicleAndHydrogenStationQueries(t *testing.T) {
|
|
now := time.Date(2026, 8, 3, 19, 30, 0, 0, time.FixedZone("CST", 8*3600))
|
|
repository := &fakeRepository{
|
|
app: AppCredential{ID: 9, Name: "vehicle-map"},
|
|
vehicles: map[string]AuthorizedVehicle{
|
|
"浙A12345": {VIN: "LTEST32960VIN0001", Plate: "浙A12345"},
|
|
"浙B67890": {VIN: "LTEST32960VIN0002", Plate: "浙B67890"},
|
|
},
|
|
realtime: map[string]RealtimeVehiclePoint{
|
|
"LTEST32960VIN0001": {VIN: "LTEST32960VIN0001", Protocol: "GB32960", Longitude: 120.1, Latitude: 30.2, SpeedKmh: 42.5, SOCPercent: float64Pointer(78.5), TotalMileageKm: 12345.6, ObservedAt: now.Add(-30 * time.Second), Online: true},
|
|
},
|
|
stations: []HydrogenStation{{ID: "1", Name: "测试加氢站", Longitude: 120.2, Latitude: 30.3}},
|
|
}
|
|
service := NewService(repository)
|
|
service.now = func() time.Time { return now }
|
|
const key = "0123456789abcdef0123456789abcdef"
|
|
|
|
vehicles, err := service.QueryRealtimeVehicles(context.Background(), key, "trace-v", RealtimeVehicleRequest{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(vehicles) != 2 || !vehicles[0].Online || vehicles[0].MotionStatus != "driving" || !vehicles[0].LocationAvailable || vehicles[0].Protocol != "GB32960" || vehicles[0].SOCPercent == nil || *vehicles[0].SOCPercent != 78.5 {
|
|
t.Fatalf("unexpected realtime vehicles: %#v", vehicles)
|
|
}
|
|
if vehicles[1].Status != StatusNoData || vehicles[1].MotionStatus != "offline" {
|
|
t.Fatalf("missing realtime row should remain explicit: %#v", vehicles[1])
|
|
}
|
|
stations, err := service.QueryHydrogenStations(context.Background(), key, "trace-s", HydrogenStationRequest{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(stations) != 1 || stations[0].Name != "测试加氢站" {
|
|
t.Fatalf("unexpected stations: %#v", stations)
|
|
}
|
|
}
|
|
|
|
func TestStationaryVehicleQueryBuildsAndRanksStayMatches(t *testing.T) {
|
|
location := time.FixedZone("CST", 8*3600)
|
|
start := time.Date(2026, 8, 6, 10, 0, 0, 0, location)
|
|
repository := &fakeRepository{
|
|
app: AppCredential{ID: 9, Name: "refuel-check"},
|
|
vehicles: map[string]AuthorizedVehicle{
|
|
"浙A12345": {VIN: "LTEST32960VIN0001", Plate: "浙A12345"},
|
|
"浙B67890": {VIN: "LTEST32960VIN0002", Plate: "浙B67890"},
|
|
},
|
|
stationary: []StationaryLocationPoint{
|
|
{VIN: "LTEST32960VIN0002", Protocol: "JT808", ObservedAt: start.Add(10 * time.Minute), Longitude: 120.00001, Latitude: 30.00001, SpeedKmh: 0},
|
|
{VIN: "LTEST32960VIN0002", Protocol: "JT808", ObservedAt: start.Add(13 * time.Minute), Longitude: 120.00001, Latitude: 30.00001, SpeedKmh: 0.1},
|
|
{VIN: "LTEST32960VIN0001", Protocol: "GB32960", ObservedAt: start.Add(time.Minute), Longitude: 120.000002, Latitude: 30.000002, SpeedKmh: 0},
|
|
{VIN: "LTEST32960VIN0001", Protocol: "GB32960", ObservedAt: start.Add(5 * time.Minute), Longitude: 120.000002, Latitude: 30.000002, SpeedKmh: 0},
|
|
{VIN: "LTEST32960VIN0001", Protocol: "YUTONG_MQTT", ObservedAt: start.Add(6 * time.Minute), Longitude: 120.000002, Latitude: 30.000002, SpeedKmh: 0},
|
|
},
|
|
}
|
|
service := NewService(repository)
|
|
service.now = func() time.Time { return start }
|
|
radiusMeters := 8.0
|
|
values, err := service.QueryStationaryVehicles(context.Background(), "0123456789abcdef0123456789abcdef", "trace-stationary", StationaryVehicleQueryRequest{
|
|
StartTime: start.Format("2006-01-02 15:04:05"), EndTime: start.Add(time.Hour).Format("2006-01-02 15:04:05"), Longitude: 120, Latitude: 30, RadiusMeters: &radiusMeters,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(values) != 2 || values[0].VIN != "LTEST32960VIN0001" || values[0].StayDurationSeconds != 300 || values[0].MatchedSamples != 3 || values[0].SourceProtocols[0] != "GB32960" || values[0].SourceProtocols[1] != "MQTT" || values[0].MatchScore <= values[1].MatchScore {
|
|
t.Fatalf("stationary matches=%#v", values)
|
|
}
|
|
if repository.stationaryRadius != radiusMeters {
|
|
t.Fatalf("stationary radius=%v, want %v", repository.stationaryRadius, radiusMeters)
|
|
}
|
|
}
|
|
|
|
func TestStationaryVehicleQueryRejectsInvalidWindowAndCoordinate(t *testing.T) {
|
|
service := NewService(&fakeRepository{})
|
|
if _, err := service.QueryStationaryVehicles(context.Background(), "0123456789abcdef0123456789abcdef", "trace", StationaryVehicleQueryRequest{StartTime: "2026-08-06 10:00:00", EndTime: "2026-08-07 10:00:01", Longitude: 120, Latitude: 30}); !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("window error=%v", err)
|
|
}
|
|
if _, err := service.QueryStationaryVehicles(context.Background(), "0123456789abcdef0123456789abcdef", "trace", StationaryVehicleQueryRequest{StartTime: "2026-08-06 10:00:00", EndTime: "2026-08-06 10:10:00", Longitude: 0, Latitude: 0}); !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("coordinate error=%v", err)
|
|
}
|
|
overRadius := 101.0
|
|
if _, err := service.QueryStationaryVehicles(context.Background(), "0123456789abcdef0123456789abcdef", "trace", StationaryVehicleQueryRequest{StartTime: "2026-08-06 10:00:00", EndTime: "2026-08-06 10:10:00", Longitude: 120, Latitude: 30, RadiusMeters: &overRadius}); !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("radius error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestStationaryVehicleQueryConvertsGCJ02Coordinate(t *testing.T) {
|
|
service := NewService(&fakeRepository{})
|
|
_, _, _, longitude, latitude, _, err := service.validateStationaryVehicleQuery(StationaryVehicleQueryRequest{
|
|
StartTime: "2026-08-06 17:53:01",
|
|
EndTime: "2026-08-06 17:55:01",
|
|
Longitude: 121.045455,
|
|
Latitude: 30.640433,
|
|
CoordinateSystem: "gcj02",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if longitude < 121.0409 || longitude > 121.0411 || latitude < 30.6425 || latitude > 30.6428 {
|
|
t.Fatalf("converted coordinate=%f,%f", longitude, latitude)
|
|
}
|
|
if _, _, _, _, _, _, err := service.validateStationaryVehicleQuery(StationaryVehicleQueryRequest{
|
|
StartTime: "2026-08-06 17:53:01", EndTime: "2026-08-06 17:55:01", Longitude: 121.045455, Latitude: 30.640433, CoordinateSystem: "BD09",
|
|
}); !errors.Is(err, ErrInvalidRequest) {
|
|
t.Fatalf("coordinateSystem error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestRealtimeVehicleOnlineUsesAnyFreshProtocol(t *testing.T) {
|
|
now := time.Date(2026, 8, 3, 19, 30, 0, 0, time.FixedZone("CST", 8*3600))
|
|
repository := &fakeRepository{
|
|
app: AppCredential{ID: 9, Name: "vehicle-map"},
|
|
vehicles: map[string]AuthorizedVehicle{
|
|
"浙A12345": {VIN: "LTEST32960VIN0001", Plate: "浙A12345"},
|
|
},
|
|
realtime: map[string]RealtimeVehiclePoint{
|
|
// The selected GB32960 point can be older while JT808/MQTT keeps the VIN online.
|
|
"LTEST32960VIN0001": {VIN: "LTEST32960VIN0001", Protocol: "GB32960", Longitude: 120.1, Latitude: 30.2, ObservedAt: now.Add(-2 * time.Minute), Online: true},
|
|
},
|
|
}
|
|
service := NewService(repository)
|
|
service.now = func() time.Time { return now }
|
|
|
|
vehicles, err := service.QueryRealtimeVehicles(context.Background(), "0123456789abcdef0123456789abcdef", "trace-any-source", RealtimeVehicleRequest{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(vehicles) != 1 || !vehicles[0].Online || vehicles[0].MotionStatus != "idle" {
|
|
t.Fatalf("fresh alternate protocol should keep vehicle online: %#v", vehicles)
|
|
}
|
|
if vehicles[0].TimeDifferenceSeconds == nil || *vehicles[0].TimeDifferenceSeconds != 120 {
|
|
t.Fatalf("selected source record time must remain auditable: %#v", vehicles[0])
|
|
}
|
|
}
|
|
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", CalculationPhase: "PRELIMINARY", AlgorithmVersion: trustedHydrogenAlgorithmVersion, EvidenceJSON: `{"lastEventTime":"2026-07-01T11:59:00+08:00"}`, UpdatedAt: "2026-07-01T12:00:00+08:00"},
|
|
},
|
|
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)
|
|
}
|
|
if hydrogen[0].VIN != "LTEST32960VIN0001" || hydrogen[1].VIN != "LTEST32960VIN0002" || hydrogen[0].StatisticsStartTime != nil || hydrogen[0].StatisticsEndTime == nil || *hydrogen[0].StatisticsEndTime != "2026-07-01T11:59:00+08:00" || hydrogen[0].UpdatedAt == nil || hydrogen[1].UpdatedAt != nil {
|
|
t.Fatalf("hydrogen identity/timestamps = %#v", hydrogen)
|
|
}
|
|
if hydrogen[0].CalculationPhase != "PRELIMINARY" || hydrogen[0].AlgorithmVersion != trustedHydrogenAlgorithmVersion {
|
|
t.Fatalf("hydrogen calculation metadata = %#v", hydrogen[0])
|
|
}
|
|
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 TestExternalHydrogenQueryPublishesSuspectCalculationAsDataAnomaly(t *testing.T) {
|
|
repository := &fakeRepository{
|
|
app: AppCredential{ID: 7, Name: "partner"},
|
|
vehicles: map[string]AuthorizedVehicle{
|
|
"粤A12345": {VIN: "LTEST32960VIN0001", Plate: "粤A12345"},
|
|
},
|
|
hydrogen: map[string]DailyHydrogen{
|
|
"LTEST32960VIN0001": {
|
|
VIN: "LTEST32960VIN0001", ConsumptionKg: 4.491, QualityStatus: "SUSPECT",
|
|
QualityReason: "燃料电池未工作期间持续压降", CalculationPhase: "FINAL", AlgorithmVersion: trustedHydrogenAlgorithmVersion,
|
|
},
|
|
},
|
|
}
|
|
service := NewService(repository)
|
|
service.now = func() time.Time { return time.Date(2026, 8, 26, 12, 0, 0, 0, time.FixedZone("CST", 8*3600)) }
|
|
result, err := service.QueryHydrogen(context.Background(), "0123456789abcdef0123456789abcdef", "trace-suspect", QueryRequest{
|
|
PlateNumbers: []string{"粤A12345"}, Date: "2026-08-26",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(result) != 1 || result[0].HydrogenConsumptionKg == nil || *result[0].HydrogenConsumptionKg != 4.491 || result[0].Status != StatusDataAnomaly || result[0].QualityStatus != "SUSPECT" || result[0].QualityReason == "" {
|
|
t.Fatalf("suspect hydrogen result = %#v", result)
|
|
}
|
|
}
|
|
|
|
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 TestMileageRollbackIsNotExposedAsNormalCarriedData(t *testing.T) {
|
|
const vin = "LNXNEGRR1SR321395"
|
|
repository := &fakeRepository{
|
|
app: AppCredential{ID: 7},
|
|
vehicles: map[string]AuthorizedVehicle{"粤A08190F": {VIN: vin, Plate: "粤A08190F"}},
|
|
priorMileage: map[string]DailyMileage{
|
|
vin: {VIN: vin, Date: "2026-08-05", Protocol: "GB32960", TotalMileageKm: 48233.3, DataTime: "2026-08-05T23:00:00+08:00", UpdatedAt: "2026-08-05T23:00:01+08:00"},
|
|
},
|
|
rollbacks: map[string]bool{dailyMileageKey(vin, "2026-08-06"): true},
|
|
}
|
|
service := NewService(repository)
|
|
result, err := service.QueryMileage(context.Background(), "0123456789abcdef0123456789abcdef", "trace-rollback", QueryRequest{
|
|
PlateNumbers: []string{"粤A08190F"}, Date: "2026-08-06",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(result) != 1 || result[0].Status != StatusDataAnomaly || result[0].DataQuality == nil || *result[0].DataQuality != mileageTotalRollbackQuality || result[0].TotalMileageKm != nil || repository.priorCalls != 0 {
|
|
t.Fatalf("rollback result=%#v priorCalls=%d", result, repository.priorCalls)
|
|
}
|
|
}
|
|
|
|
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: "2023-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())
|
|
}
|
|
}
|
|
|
|
// Preserve the already deployed three-year window when adding new public APIs.
|
|
func TestMileageRangePreservesProductionThreeYearWindow(t *testing.T) {
|
|
s := NewService(&fakeRepository{})
|
|
_, _, _, _, _, _, _, err := s.validateMileageRange(MileageRangeRequest{StartDate: "2023-07-01", EndDate: "2026-06-30", PageSize: 1})
|
|
if err != nil {
|
|
t.Fatalf("production 1096-day window rejected: %v", err)
|
|
}
|
|
}
|