package stats import ( "context" "database/sql" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" ) func TestMetricRepositoryQueriesDailyMetricsWithFilters(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("sqlmock.New() error = %v", err) } defer db.Close() 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("LKLG7C4E3NA774736", "JT808", "2026-07-01", "2026-07-01", 20, 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", }).AddRow( "LKLG7C4E3NA774736", "LKLG7C4E3NA774736", time.Date(2026, 7, 1, 0, 0, 0, 0, time.FixedZone("Asia/Shanghai", 8*3600)), "JT808", MetricDailyTotalMileageKM, 12345.6, "km", 12345.6, 12345.6, 13, "TOTAL_MILEAGE_DIFF", time.Date(2026, 7, 1, 22, 49, 11, 0, time.FixedZone("Asia/Shanghai", 8*3600)), time.Date(2026, 7, 1, 23, 9, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)), )) repository := NewMetricRepository(db) rows, err := repository.Query(context.Background(), MetricQuery{ VIN: "LKLG7C4E3NA774736", Protocol: "JT808", DateFrom: "2026-07-01", DateTo: "2026-07-01", Limit: 20, }) if err != nil { t.Fatalf("Query() error = %v", err) } if len(rows) != 1 { t.Fatalf("row count = %d", len(rows)) } if rows[0].MetricKey != MetricDailyTotalMileageKM || rows[0].MetricValue != 12345.6 { t.Fatalf("unexpected row: %#v", rows[0]) } if rows[0].VehicleKey != "LKLG7C4E3NA774736" { t.Fatalf("vehicle key = %q", rows[0].VehicleKey) } if rows[0].StatDate != "2026-07-01" || rows[0].UpdatedAt != "2026-07-01 23:09:36" { t.Fatalf("unexpected time formatting: %#v", rows[0]) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestMetricHandlerReturnsDailyMetrics(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("LB9A32A21R0LS1707", "GB32960", "2020-07-01", "2020-07-01"). WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(42)) 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("LB9A32A21R0LS1707", "GB32960", "2020-07-01", "2020-07-01", 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", }).AddRow( "LB9A32A21R0LS1707", "LB9A32A21R0LS1707", "2020-07-01", "GB32960", MetricDailyMileageKM, 0.0, "km", 53490.9, 53490.9, 3, "TOTAL_MILEAGE_DIFF", "2026-07-01 22:07:58", "2026-07-01 22:28:25", )) handler := NewMetricHandler(NewMetricRepository(db)) request := httptest.NewRequest(http.MethodGet, "/api/stats/daily-metrics?vin=LB9A32A21R0LS1707&protocol=GB32960&dateFrom=2020-07-01&dateTo=2020-07-01", 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() for _, want := range []string{`"vin":"LB9A32A21R0LS1707"`, `"metric_key":"daily_mileage_km"`, `"total":42`} { if !strings.Contains(body, want) { t.Fatalf("response missing %s: %s", want, body) } } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestMetricHandlerFiltersByVehicleKey(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("JT808:013307811254", "JT808"). WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(12)) 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("JT808:013307811254", "JT808", 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", }).AddRow( "JT808:013307811254", "", "2026-07-02", "JT808", MetricDailyTotalMileageKM, 10985.7, "km", 10985.7, 10985.7, 1, "TOTAL_MILEAGE_DIFF", "2026-07-02 00:03:00", "2026-07-02 00:03:00", )) handler := NewMetricHandler(NewMetricRepository(db)) request := httptest.NewRequest(http.MethodGet, "/api/stats/daily-metrics?vehicleKey=JT808:013307811254&protocol=JT808", 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() for _, want := range []string{`"vehicle_key":"JT808:013307811254"`, `"vin":""`, `"metric_value":10985.7`} { if !strings.Contains(body, want) { t.Fatalf("response missing %s: %s", want, body) } } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } 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) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusBadRequest { t.Fatalf("status = %d body=%s", response.Code, response.Body.String()) } }