Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/internal/openplatform/service_test.go
2026-07-27 16:46:15 +08:00

547 lines
24 KiB
Go

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