387 lines
14 KiB
Go
387 lines
14 KiB
Go
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 COUNT\\(\\*\\) FROM lingniu_vehicle_ts.raw_frames").
|
|
WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(38))
|
|
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":38`} {
|
|
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("SELECT COUNT\\(\\*\\) FROM lingniu_vehicle_ts.raw_frames").
|
|
WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(11))
|
|
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":11`} {
|
|
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 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 {
|
|
t.Fatalf("sqlmock.New() error = %v", err)
|
|
}
|
|
defer db.Close()
|
|
mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM lingniu_vehicle_ts.vehicle_locations").
|
|
WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(17))
|
|
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":17`} {
|
|
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 TestMileagePointHandlerReturnsMileageByVehicleKey(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.vehicle_mileage_points").
|
|
WillReturnRows(sqlmock.NewRows([]string{"total"}).AddRow(19))
|
|
mock.ExpectQuery("vehicle_key = 'JT808:013307811350'").
|
|
WillReturnRows(sqlmock.NewRows([]string{
|
|
"ts", "event_id", "frame_id", "received_at", "total_mileage_km", "speed_kmh", "longitude", "latitude",
|
|
"protocol", "vehicle_key", "vin", "phone", "device_id",
|
|
}).AddRow(
|
|
"2026-07-02 00:18:22", "event-3", "go_frame", "2026-07-02 00:22:43",
|
|
8792.8, 8.0, 121.07764, 30.585928,
|
|
"JT808", "JT808:013307811350", "", "013307811350", "",
|
|
))
|
|
|
|
handler := NewMileagePointHandler(NewMileagePointRepository(db, "lingniu_vehicle_ts"))
|
|
request := httptest.NewRequest(http.MethodGet, "/api/history/mileage-points?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"`, `"total_mileage_km":8792.8`, `"speed_kmh":8`, `"total":19`} {
|
|
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 TestParseRawFrameQueryAcceptsDatetimeLocalValues(t *testing.T) {
|
|
request := httptest.NewRequest(http.MethodGet, "/api/history/raw-frames?dateFrom=2026-07-01T00:00:00&dateTo=2026-07-02T00:00:00", nil)
|
|
|
|
query, err := parseRawFrameQuery(request)
|
|
if err != nil {
|
|
t.Fatalf("parseRawFrameQuery() error = %v", err)
|
|
}
|
|
if query.DateFrom != "2026-06-30 16:00:00" || query.DateTo != "2026-07-01 16:00:00" {
|
|
t.Fatalf("date range = %q -> %q", query.DateFrom, query.DateTo)
|
|
}
|
|
}
|
|
|
|
func TestNormalizeDateTimeLiteralConvertsInputToTDengineUTCTime(t *testing.T) {
|
|
for raw, want := range map[string]string{
|
|
"2026-07-01T00:00:00": "2026-06-30 16:00:00",
|
|
"2026-07-01 00:00:00": "2026-06-30 16:00:00",
|
|
"2026-07-01T00:00:00+08:00": "2026-06-30 16:00:00",
|
|
"2026-06-30T16:00:00Z": "2026-06-30 16:00:00",
|
|
} {
|
|
if got := normalizeDateTimeLiteral(raw); got != want {
|
|
t.Fatalf("normalizeDateTimeLiteral(%q) = %q, want %q", raw, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 TestBuildMileagePointSQLUsesLiteralsForTDengine(t *testing.T) {
|
|
sqlText, args := buildMileagePointSQL("lingniu_vehicle_ts.vehicle_mileage_points", MileagePointQuery{
|
|
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-01 16:00:00'",
|
|
"LIMIT 20 OFFSET 5",
|
|
} {
|
|
if !strings.Contains(sqlText, want) {
|
|
t.Fatalf("sql missing %s: %s", want, sqlText)
|
|
}
|
|
}
|
|
}
|
|
|
|
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-01 16: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-01T00:00:00",
|
|
DateTo: "2026-07-01T23: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",
|
|
"ts >= '2026-06-30 16:00:00'",
|
|
"ts <= '2026-07-01 15:59:59'",
|
|
"LIMIT 20 OFFSET 5",
|
|
} {
|
|
if !strings.Contains(sqlText, want) {
|
|
t.Fatalf("sql missing %s: %s", want, sqlText)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildRawFrameSQLCanOrderByReceivedAt(t *testing.T) {
|
|
sqlText, args := buildRawFrameSQL("lingniu_vehicle_ts.raw_frames", RawFrameQuery{
|
|
Protocol: "JT808",
|
|
OrderBy: "receivedAt",
|
|
Limit: 1,
|
|
})
|
|
if len(args) != 0 {
|
|
t.Fatalf("expected no query args for TDengine, got %#v", args)
|
|
}
|
|
if !strings.Contains(sqlText, "ORDER BY received_at DESC LIMIT 1 OFFSET 0") {
|
|
t.Fatalf("sql should order by received_at: %s", sqlText)
|
|
}
|
|
}
|