feat(auth): enforce vehicle grant time boundaries

This commit is contained in:
lingniu
2026-07-16 15:44:29 +08:00
parent a541d10c7b
commit 4688abadef
11 changed files with 580 additions and 35 deletions

View File

@@ -48,11 +48,14 @@ func TestHistoryExportIndexSurvivesServiceRestart(t *testing.T) {
type countingStore struct {
*MockStore
vehiclesCalls int
vehicleRealtimeCalls int
overviewBatchCalls int
lastVehicleQuery url.Values
lastRealtimeQuery url.Values
vehiclesCalls int
vehicleRealtimeCalls int
overviewBatchCalls int
lastVehicleQuery url.Values
lastRealtimeQuery url.Values
lastHistoryQuery url.Values
lastDailyMileageQuery url.Values
lastMileageStatisticsQuery url.Values
}
func newCountingStore() *countingStore {
@@ -71,6 +74,21 @@ func (s *countingStore) VehicleRealtime(ctx context.Context, query url.Values) (
return s.MockStore.VehicleRealtime(ctx, query)
}
func (s *countingStore) HistoryLocationsFromTDengine(ctx context.Context, query url.Values) (Page[HistoryLocationRow], error) {
s.lastHistoryQuery = cloneValues(query)
return s.MockStore.HistoryLocationsFromTDengine(ctx, query)
}
func (s *countingStore) DailyMileage(ctx context.Context, query url.Values) (Page[DailyMileageRow], error) {
s.lastDailyMileageQuery = cloneValues(query)
return s.MockStore.DailyMileage(ctx, query)
}
func (s *countingStore) MileageStatistics(ctx context.Context, query url.Values) (MileageStatistics, error) {
s.lastMileageStatisticsQuery = cloneValues(query)
return s.MockStore.MileageStatistics(ctx, query)
}
func TestCustomerVehicleScopeIsInjectedAndExplicitBypassIsDenied(t *testing.T) {
store := newCountingStore()
service := NewService(store)
@@ -88,6 +106,86 @@ func TestCustomerVehicleScopeIsInjectedAndExplicitBypassIsDenied(t *testing.T) {
}
}
func TestCustomerHistoryIsClampedToVehicleGrantStart(t *testing.T) {
store := newCountingStore()
service := NewService(store)
validFrom := time.Date(2026, 7, 10, 12, 34, 56, 0, time.FixedZone("Asia/Shanghai", 8*60*60))
principal := Principal{
Name: "客户甲", Role: "customer", UserType: "customer",
VehicleVINs: []string{"LB9A32A24R0LS1426"},
VehicleGrants: []VehicleGrant{{VIN: "LB9A32A24R0LS1426", ValidFrom: validFrom}},
}
ctx := WithPrincipal(context.Background(), principal)
_, err := service.HistoryLocations(ctx, url.Values{
"vin": {"LB9A32A24R0LS1426"},
"dateFrom": {"2026-07-01T00:00:00+08:00"},
"dateTo": {"2026-07-11T00:00:00+08:00"},
})
if err != nil {
t.Fatalf("history query failed: %v", err)
}
if got := store.lastHistoryQuery.Get("dateFrom"); got != validFrom.Format(time.RFC3339) {
t.Fatalf("dateFrom=%q want=%q", got, validFrom.Format(time.RFC3339))
}
}
func TestCustomerHistoryBeforeGrantIsForbidden(t *testing.T) {
service := NewService(newCountingStore())
validFrom := time.Date(2026, 7, 10, 12, 34, 56, 0, time.FixedZone("Asia/Shanghai", 8*60*60))
ctx := WithPrincipal(context.Background(), Principal{
Name: "客户甲", Role: "customer", UserType: "customer",
VehicleVINs: []string{"LB9A32A24R0LS1426"},
VehicleGrants: []VehicleGrant{{VIN: "LB9A32A24R0LS1426", ValidFrom: validFrom}},
})
_, err := service.HistoryLocations(ctx, url.Values{
"vin": {"LB9A32A24R0LS1426"},
"dateFrom": {"2026-07-01T00:00:00+08:00"},
"dateTo": {"2026-07-10T12:34:56+08:00"},
})
clientErr, ok := asClientError(err)
if !ok || clientErr.Code != "HISTORY_BEFORE_AUTHORIZATION" {
t.Fatalf("expected HISTORY_BEFORE_AUTHORIZATION, err=%v", err)
}
}
func TestCustomerMileageStartsAtFirstCompleteAuthorizedDay(t *testing.T) {
store := newCountingStore()
service := NewService(store)
validFrom := time.Date(2026, 7, 10, 12, 34, 56, 0, time.FixedZone("Asia/Shanghai", 8*60*60))
ctx := WithPrincipal(context.Background(), Principal{
Name: "客户甲", Role: "customer", UserType: "customer",
VehicleVINs: []string{"LB9A32A24R0LS1426"},
VehicleGrants: []VehicleGrant{{VIN: "LB9A32A24R0LS1426", ValidFrom: validFrom}},
})
if _, err := service.DailyMileage(ctx, url.Values{
"vins": {"LB9A32A24R0LS1426"},
"dateFrom": {"2026-07-01"},
"dateTo": {"2026-07-15"},
}); err != nil {
t.Fatalf("daily mileage failed: %v", err)
}
if got := store.lastDailyMileageQuery.Get("scopeVehicleFrom"); got != `{"LB9A32A24R0LS1426":"2026-07-11"}` {
t.Fatalf("scopeVehicleFrom=%q", got)
}
}
func TestCustomerMileageFailsClosedWithoutGrantTime(t *testing.T) {
service := NewService(newCountingStore())
ctx := WithPrincipal(context.Background(), Principal{
Name: "客户甲", Role: "customer", UserType: "customer",
VehicleVINs: []string{"LB9A32A24R0LS1426"},
})
_, err := service.MileageStatistics(ctx, url.Values{
"vins": {"LB9A32A24R0LS1426"},
"dateFrom": {"2026-07-01"},
"dateTo": {"2026-07-15"},
})
clientErr, ok := asClientError(err)
if !ok || clientErr.Code != "HISTORY_SCOPE_UNAVAILABLE" {
t.Fatalf("expected HISTORY_SCOPE_UNAVAILABLE, err=%v", err)
}
}
func (s *countingStore) VehicleServiceOverviews(ctx context.Context, query VehicleOverviewBatchQuery) (Page[VehicleServiceOverview], error) {
s.overviewBatchCalls++
return s.MockStore.VehicleServiceOverviews(ctx, query)