diff --git a/go/vehicle-gateway/internal/history/query.go b/go/vehicle-gateway/internal/history/query.go index c2f08cd5..da35b069 100644 --- a/go/vehicle-gateway/internal/history/query.go +++ b/go/vehicle-gateway/internal/history/query.go @@ -169,7 +169,7 @@ func (r *RawFrameRepository) Query(ctx context.Context, query RawFrameQuery) ([] } defer rows.Close() - var out []RawFrameRow + out := make([]RawFrameRow, 0) for rows.Next() { var row RawFrameRow var ts scanDateTime @@ -222,7 +222,7 @@ func (r *LocationRepository) Query(ctx context.Context, query LocationQuery) ([] } defer rows.Close() - var out []LocationRow + out := make([]LocationRow, 0) for rows.Next() { var row LocationRow var ts scanDateTime @@ -282,7 +282,7 @@ func (r *MileagePointRepository) Query(ctx context.Context, query MileagePointQu } defer rows.Close() - var out []MileagePointRow + out := make([]MileagePointRow, 0) for rows.Next() { var row MileagePointRow var ts scanDateTime diff --git a/go/vehicle-gateway/internal/history/query_test.go b/go/vehicle-gateway/internal/history/query_test.go index db411905..1c206078 100644 --- a/go/vehicle-gateway/internal/history/query_test.go +++ b/go/vehicle-gateway/internal/history/query_test.go @@ -135,6 +135,39 @@ func TestRawFrameHandlerFiltersByVehicleKey(t *testing.T) { } } +func TestRawFrameHandlerReturnsEmptyItemsArrayWhenNoRows(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New() error = %v", err) + } + defer db.Close() + mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM lingniu_vehicle_ts.raw_frames"). + WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(0)) + mock.ExpectQuery("SELECT ts, frame_id, event_id, message_id, event_time, received_at, raw_size_bytes, raw_hex, raw_text, parsed_json, fields_json, parse_status, parse_error, source_endpoint, protocol, vehicle_key, vin, phone, device_id FROM lingniu_vehicle_ts.raw_frames"). + WillReturnRows(sqlmock.NewRows([]string{ + "ts", "frame_id", "event_id", "message_id", "event_time", "received_at", "raw_size_bytes", + "raw_hex", "raw_text", "parsed_json", "fields_json", "parse_status", "parse_error", "source_endpoint", + "protocol", "vehicle_key", "vin", "phone", "device_id", + })) + + handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts")) + request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?protocol=NOT_EXISTS&limit=1", nil) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + if response.Code != http.StatusOK { + t.Fatalf("status = %d body=%s", response.Code, response.Body.String()) + } + body := response.Body.String() + if !strings.Contains(body, `"items":[]`) || strings.Contains(body, `"items":null`) { + t.Fatalf("expected empty items array, got: %s", body) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("sql expectations: %v", err) + } +} + func TestLocationHandlerReturnsLocationsByVehicleKey(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { diff --git a/go/vehicle-gateway/internal/stats/query.go b/go/vehicle-gateway/internal/stats/query.go index e735ec47..052ea3da 100644 --- a/go/vehicle-gateway/internal/stats/query.go +++ b/go/vehicle-gateway/internal/stats/query.go @@ -63,7 +63,7 @@ func (r *MetricRepository) Query(ctx context.Context, query MetricQuery) ([]Metr } defer rows.Close() - var out []MetricRow + out := make([]MetricRow, 0) for rows.Next() { var row MetricRow var statDate scanDate diff --git a/go/vehicle-gateway/internal/stats/query_test.go b/go/vehicle-gateway/internal/stats/query_test.go index da354eb0..ca7e4ab7 100644 --- a/go/vehicle-gateway/internal/stats/query_test.go +++ b/go/vehicle-gateway/internal/stats/query_test.go @@ -137,6 +137,41 @@ func TestMetricHandlerFiltersByVehicleKey(t *testing.T) { } } +func TestMetricHandlerReturnsEmptyItemsArrayWhenNoRows(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock.New() error = %v", err) + } + defer db.Close() + mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM vehicle_daily_metric"). + WithArgs("YUTONG_MQTT"). + WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(0)) + mock.ExpectQuery("SELECT vehicle_key, vin, stat_date, protocol, metric_key, metric_value, metric_unit, first_total_mileage_km, latest_total_mileage_km, sample_count, calculation_method, created_at, updated_at FROM vehicle_daily_metric"). + WithArgs("YUTONG_MQTT", 50, 0). + WillReturnRows(sqlmock.NewRows([]string{ + "vehicle_key", "vin", "stat_date", "protocol", "metric_key", "metric_value", "metric_unit", + "first_total_mileage_km", "latest_total_mileage_km", "sample_count", "calculation_method", + "created_at", "updated_at", + })) + + handler := NewMetricHandler(NewMetricRepository(db)) + request := httptest.NewRequest(http.MethodGet, "/api/stats/daily-metrics?protocol=YUTONG_MQTT", nil) + response := httptest.NewRecorder() + + handler.ServeHTTP(response, request) + + if response.Code != http.StatusOK { + t.Fatalf("status = %d body=%s", response.Code, response.Body.String()) + } + body := response.Body.String() + if !strings.Contains(body, `"items":[]`) || strings.Contains(body, `"items":null`) { + t.Fatalf("expected empty items array, got: %s", body) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("sql expectations: %v", err) + } +} + func TestMetricHandlerRejectsInvalidPagination(t *testing.T) { handler := NewMetricHandler(NewMetricRepository(&sql.DB{})) request := httptest.NewRequest(http.MethodGet, "/api/stats/daily-metrics?limit=2001", nil)