package history import ( "context" "database/sql" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/DATA-DOG/go-sqlmock" ) func TestRawFrameRepositoryQueriesRawFramesWithFilters(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("sqlmock.New() error = %v", err) } defer db.Close() 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", }).AddRow( time.Date(2026, 7, 1, 23, 25, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)), "go_frame", "event-1", 0x0200, time.Date(2026, 7, 1, 23, 25, 36, 0, time.FixedZone("Asia/Shanghai", 8*3600)), time.Date(2026, 7, 1, 23, 25, 37, 0, time.FixedZone("Asia/Shanghai", 8*3600)), 64, "7E0200", "", `{"header":{"message_id":"0x0200"}}`, `{"speed_kmh":0}`, "OK", "", "222.66.200.68:29646", "JT808", "013079963379", "LKLG7C4E3NA774736", "013079963379", "9963379", )) repository := NewRawFrameRepository(db, "lingniu_vehicle_ts") rows, err := repository.Query(context.Background(), RawFrameQuery{ Protocol: "JT808", VIN: "LKLG7C4E3NA774736", DateFrom: "2026-07-01 00:00:00", DateTo: "2026-07-01 23:59:59", 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].MessageID != 512 || rows[0].MessageIDHex != "0x0200" || rows[0].ParsedJSON == "" { t.Fatalf("unexpected row: %#v", rows[0]) } if rows[0].TS != "2026-07-01 23:25:36" { t.Fatalf("timestamp = %q", rows[0].TS) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("sql expectations: %v", err) } } func TestRawFrameHandlerReturnsRawFrames(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("sqlmock.New() error = %v", err) } defer db.Close() 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", }).AddRow( "2026-07-01 22:28:25", "go_frame", "event-2", 2, "2026-07-01 22:28:25", "2026-07-01 22:28:25", 128, "2323", "", `{"command":"REALTIME"}`, `{"total_mileage_km":53490.9}`, "OK", "", "8.134.95.166:53702", "GB32960", "LB9A32A21R0LS1707", "LB9A32A21R0LS1707", "", "", )) handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts")) request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?protocol=GB32960&vin=LB9A32A21R0LS1707&limit=5", 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"`, `"message_id_hex":"0x0002"`, `"total":1`} { 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 TestRawFrameHandlerFiltersByVehicleKey(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("sqlmock.New() error = %v", err) } defer db.Close() mock.ExpectQuery("vehicle_key = 'JT808:013307811350'"). 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", }).AddRow( "2026-07-02 00:18:22", "go_frame", "event-3", 0x0200, "2026-07-02 00:18:22", "2026-07-02 00:22:43", 63, "7E0200", "", `{"header":{"message_id":"0x0200"}}`, `{"total_mileage_km":8792.8}`, "OK", "", "115.231.168.135:22170", "JT808", "JT808:013307811350", "", "013307811350", "", )) handler := NewRawFrameHandler(NewRawFrameRepository(db, "lingniu_vehicle_ts")) request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?vehicleKey=JT808:013307811350&protocol=JT808&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() for _, want := range []string{`"vehicle_key":"JT808:013307811350"`, `"phone":"013307811350"`, `"total":1`} { 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 TestLocationHandlerReturnsLocationsByVehicleKey(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("sqlmock.New() error = %v", err) } defer db.Close() mock.ExpectQuery("vehicle_key = 'JT808:013307811350'"). WillReturnRows(sqlmock.NewRows([]string{ "ts", "event_id", "frame_id", "received_at", "longitude", "latitude", "altitude_m", "speed_kmh", "direction_deg", "alarm_flag", "status_flag", "total_mileage_km", "protocol", "vehicle_key", "vin", "phone", "device_id", }).AddRow( "2026-07-02 00:18:22", "event-3", "go_frame", "2026-07-02 00:22:43", 121.07764, 30.585928, 11.0, 8.0, 171, 0, 4718595, 8792.8, "JT808", "JT808:013307811350", "", "013307811350", "", )) handler := NewLocationHandler(NewLocationRepository(db, "lingniu_vehicle_ts")) request := httptest.NewRequest(http.MethodGet, "/api/history/locations?vehicleKey=JT808:013307811350&protocol=JT808&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() for _, want := range []string{`"vehicle_key":"JT808:013307811350"`, `"longitude":121.07764`, `"total_mileage_km":8792.8`, `"total":1`} { 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 TestRawFrameHandlerRejectsInvalidLimit(t *testing.T) { handler := NewRawFrameHandler(NewRawFrameRepository(&sql.DB{}, "")) request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?limit=501", nil) response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusBadRequest { t.Fatalf("status = %d body=%s", response.Code, response.Body.String()) } } func TestParseMessageIDSupportsDecimalAndHex(t *testing.T) { for raw, want := range map[string]int64{ "512": 512, "0x0200": 512, "0X0100": 256, } { got, ok := parseMessageID(raw) if !ok || got != want { t.Fatalf("parseMessageID(%q) = %d,%v want %d,true", raw, got, ok, want) } } } func TestBuildLocationSQLUsesLiteralsForTDengine(t *testing.T) { sqlText, args := buildLocationSQL("lingniu_vehicle_ts.vehicle_locations", LocationQuery{ Protocol: "JT808", VehicleKey: "JT808:013307811350", DateFrom: "2026-07-02 00:00:00", DateTo: "2026-07-02 23:59:59", Limit: 20, Offset: 5, }) if len(args) != 0 { t.Fatalf("expected no query args for TDengine, got %#v", args) } for _, want := range []string{ "protocol = 'JT808'", "vehicle_key = 'JT808:013307811350'", "ts >= '2026-07-02 00:00:00'", "LIMIT 20 OFFSET 5", } { if !strings.Contains(sqlText, want) { t.Fatalf("sql missing %s: %s", want, sqlText) } } } func TestBuildRawFrameSQLUsesLiteralsForTDengine(t *testing.T) { sqlText, args := buildRawFrameSQL("lingniu_vehicle_ts.raw_frames", RawFrameQuery{ Protocol: "JT808", VehicleKey: "JT808:013307811350", VIN: "VIN'1", MessageID: "0x0200", DateFrom: "2026-07-01 00:00:00", DateTo: "2026-07-01 23:59:59", Limit: 20, Offset: 5, }) if len(args) != 0 { t.Fatalf("expected no query args for TDengine, got %#v", args) } for _, want := range []string{ "protocol = 'JT808'", "vehicle_key = 'JT808:013307811350'", "vin = 'VIN''1'", "message_id = 512", "LIMIT 20 OFFSET 5", } { if !strings.Contains(sqlText, want) { t.Fatalf("sql missing %s: %s", want, sqlText) } } }