package main import ( "database/sql" "encoding/base64" "errors" "path/filepath" "strings" "testing" "gorm.io/gorm" ) func TestOpenStoreCreatesTables(t *testing.T) { st := openTestStore(t) defer st.Close() for _, table := range []string{"users", "login_log", "discard_details", "node_blocking", "ip_blocking", "forbidden_word_blocking", "nodeinfo", "map_report", "text_message", "position", "telemetry", "routing", "traceroute"} { var name string if err := rawTestDB(t, st).QueryRow("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?", table).Scan(&name); err != nil { t.Fatalf("%s table missing: %v", table, err) } if name != table { t.Fatalf("table name = %q, want %s", name, table) } } var oldCount int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'nodeinfo_map'").Scan(&oldCount); err != nil { t.Fatal(err) } if oldCount != 0 { t.Fatalf("nodeinfo_map table count = %d, want 0", oldCount) } } func TestUpsertNodeInfoInsertsAndUpdatesSameNode(t *testing.T) { st := openTestStore(t) defer st.Close() first := nodeInfoTestRecord("first name") if err := st.UpsertNodeInfo(first); err != nil { t.Fatalf("first UpsertNodeInfo() error = %v", err) } second := nodeInfoTestRecord("second name") second["short_name"] = "snd" if err := st.UpsertNodeInfo(second); err != nil { t.Fatalf("second UpsertNodeInfo() error = %v", err) } var count int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM nodeinfo WHERE node_id = ?", "!12345678").Scan(&count); err != nil { t.Fatal(err) } if count != 1 { t.Fatalf("nodeinfo row count = %d, want 1", count) } var longName, shortName, content string if err := rawTestDB(t, st).QueryRow("SELECT long_name, short_name, content_json FROM nodeinfo WHERE node_id = ?", "!12345678").Scan(&longName, &shortName, &content); err != nil { t.Fatal(err) } if longName != "second name" || shortName != "snd" { t.Fatalf("nodeinfo names = %q/%q, want second name/snd", longName, shortName) } if !strings.Contains(content, "second name") { t.Fatalf("content_json = %q, want updated content", content) } } func TestUpsertMapReportInsertsAndUpdatesSameNode(t *testing.T) { st := openTestStore(t) defer st.Close() first := mapReportTestRecord("first map") if err := st.UpsertMapReport(first); err != nil { t.Fatalf("first UpsertMapReport() error = %v", err) } second := mapReportTestRecord("second map") second["latitude"] = 43.5 if err := st.UpsertMapReport(second); err != nil { t.Fatalf("second UpsertMapReport() error = %v", err) } var count int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM map_report WHERE node_id = ?", "!12345678").Scan(&count); err != nil { t.Fatal(err) } if count != 1 { t.Fatalf("map_report row count = %d, want 1", count) } var longName string var latitude float64 var opted sql.NullBool if err := rawTestDB(t, st).QueryRow("SELECT long_name, latitude, has_opted_report_location FROM map_report WHERE node_id = ?", "!12345678").Scan(&longName, &latitude, &opted); err != nil { t.Fatal(err) } if longName != "second map" || latitude != 43.5 { t.Fatalf("map_report row = %q/%v, want second map/43.5", longName, latitude) } if !opted.Valid || opted.Bool { t.Fatalf("has_opted_report_location = %+v, want valid false", opted) } } func TestListMapReportsFiltersByBounds(t *testing.T) { st := openTestStore(t) defer st.Close() inside := mapReportTestRecord("inside") inside["from"] = "!00000001" inside["from_num"] = uint32(1) inside["latitude"] = 10.5 inside["longitude"] = 20.5 outside := mapReportTestRecord("outside") outside["from"] = "!00000002" outside["from_num"] = uint32(2) outside["latitude"] = 50.0 outside["longitude"] = 20.5 missingCoords := mapReportTestRecord("missing coords") missingCoords["from"] = "!00000003" missingCoords["from_num"] = uint32(3) delete(missingCoords, "latitude") delete(missingCoords, "longitude") for _, record := range []map[string]any{inside, outside, missingCoords} { if err := st.UpsertMapReport(record); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } } minLat, maxLat := 10.0, 11.0 minLng, maxLng := 20.0, 21.0 opts := listOptions{Limit: 100, MinLat: &minLat, MaxLat: &maxLat, MinLng: &minLng, MaxLng: &maxLng} rows, err := st.ListMapReports(opts) if err != nil { t.Fatalf("ListMapReports() error = %v", err) } if len(rows) != 1 || rows[0].NodeID != "!00000001" { t.Fatalf("ListMapReports() = %+v, want only !00000001", rows) } total, err := st.CountMapReports(opts) if err != nil || total != 1 { t.Fatalf("CountMapReports() = %d, %v, want 1, nil", total, err) } } func TestListMapReportsFiltersAcrossAntimeridian(t *testing.T) { st := openTestStore(t) defer st.Close() west := mapReportTestRecord("west") west["from"] = "!00000001" west["from_num"] = uint32(1) west["latitude"] = 0.0 west["longitude"] = 175.0 east := mapReportTestRecord("east") east["from"] = "!00000002" east["from_num"] = uint32(2) east["latitude"] = 0.0 east["longitude"] = -175.0 outside := mapReportTestRecord("outside") outside["from"] = "!00000003" outside["from_num"] = uint32(3) outside["latitude"] = 0.0 outside["longitude"] = 0.0 for _, record := range []map[string]any{west, east, outside} { if err := st.UpsertMapReport(record); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } } minLat, maxLat := -10.0, 10.0 minLng, maxLng := 170.0, -170.0 rows, err := st.ListMapReports(listOptions{Limit: 100, MinLat: &minLat, MaxLat: &maxLat, MinLng: &minLng, MaxLng: &maxLng}) if err != nil { t.Fatalf("ListMapReports() error = %v", err) } if len(rows) != 2 { t.Fatalf("ListMapReports() length = %d, want 2: %+v", len(rows), rows) } seen := map[string]bool{} for _, row := range rows { seen[row.NodeID] = true } if !seen["!00000001"] || !seen["!00000002"] || seen["!00000003"] { t.Fatalf("seen nodes = %+v, want west/east only", seen) } } func TestListMapReportViewportReturnsPointsBelowThreshold(t *testing.T) { st := openTestStore(t) defer st.Close() for index := 0; index < 3; index++ { record := mapReportTestRecord("point") record["from"] = "!0000000" + string(rune('1'+index)) record["from_num"] = uint32(index + 1) record["latitude"] = float64(index) record["longitude"] = float64(index) if err := st.UpsertMapReport(record); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } } minLat, maxLat := -1.0, 5.0 minLng, maxLng := -1.0, 5.0 result, err := st.ListMapReportViewport(mapReportViewportOptions{ ListOptions: listOptions{MinLat: &minLat, MaxLat: &maxLat, MinLng: &minLng, MaxLng: &maxLng}, Zoom: 8, Limit: 1000, ClusterThreshold: 10, TargetCells: 64, }) if err != nil { t.Fatalf("ListMapReportViewport() error = %v", err) } if result.Mode != "points" || result.Total != 3 || len(result.Points) != 3 || len(result.Clusters) != 0 { t.Fatalf("viewport result = %+v, want 3 points", result) } } func TestListMapReportViewportReturnsClustersAboveThreshold(t *testing.T) { st := openTestStore(t) defer st.Close() for index := 0; index < 4; index++ { record := mapReportTestRecord("cluster") record["from"] = "!0000000" + string(rune('1'+index)) record["from_num"] = uint32(index + 1) record["latitude"] = 10.0 + float64(index)*0.01 record["longitude"] = 20.0 + float64(index)*0.01 if err := st.UpsertMapReport(record); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } } minLat, maxLat := 9.0, 11.0 minLng, maxLng := 19.0, 21.0 result, err := st.ListMapReportViewport(mapReportViewportOptions{ ListOptions: listOptions{MinLat: &minLat, MaxLat: &maxLat, MinLng: &minLng, MaxLng: &maxLng}, Zoom: 4, Limit: 1000, ClusterThreshold: 2, TargetCells: 1, }) if err != nil { t.Fatalf("ListMapReportViewport() error = %v", err) } if result.Mode != "clusters" || result.Total != 4 || len(result.Clusters) != 1 || result.Clusters[0].Count != 4 { t.Fatalf("viewport result = %+v, want one cluster with count 4", result) } if result.Clusters[0].Latitude < 10 || result.Clusters[0].Latitude > 10.1 || result.Clusters[0].Longitude < 20 || result.Clusters[0].Longitude > 20.1 { t.Fatalf("cluster center = %v/%v, want center near inserted points", result.Clusters[0].Latitude, result.Clusters[0].Longitude) } } func TestNodeInfoAndMapReportAreStoredSeparately(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.UpsertNodeInfo(nodeInfoTestRecord("node name")); err != nil { t.Fatalf("UpsertNodeInfo() error = %v", err) } if err := st.UpsertMapReport(mapReportTestRecord("map name")); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } var nodeLongName, userID, publicKey string if err := rawTestDB(t, st).QueryRow("SELECT long_name, user_id, public_key FROM nodeinfo WHERE node_id = ?", "!12345678").Scan(&nodeLongName, &userID, &publicKey); err != nil { t.Fatal(err) } if nodeLongName != "map name" || userID != "!12345678" || publicKey != "abcd" { t.Fatalf("nodeinfo row = %q/%q/%q, want synced map name plus node-only fields", nodeLongName, userID, publicKey) } var mapLongName, firmware string var latitude float64 if err := rawTestDB(t, st).QueryRow("SELECT long_name, firmware_version, latitude FROM map_report WHERE node_id = ?", "!12345678").Scan(&mapLongName, &firmware, &latitude); err != nil { t.Fatal(err) } if mapLongName != "map name" || firmware != "1.2.3" || latitude != 42.5 { t.Fatalf("map_report row = %q/%q/%v, want map fields", mapLongName, firmware, latitude) } } func TestUpsertNodeInfoUpdatesExistingMapReportFields(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.UpsertMapReport(mapReportTestRecord("map name")); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } node := nodeInfoTestRecord("node name") node["short_name"] = "nod" node["hw_model"] = "NODE_HW" node["role"] = "CLIENT" if err := st.UpsertNodeInfo(node); err != nil { t.Fatalf("UpsertNodeInfo() error = %v", err) } var longName, shortName, hwModel, role, firmware string var latitude float64 if err := rawTestDB(t, st).QueryRow("SELECT long_name, short_name, hw_model, role, firmware_version, latitude FROM map_report WHERE node_id = ?", "!12345678").Scan(&longName, &shortName, &hwModel, &role, &firmware, &latitude); err != nil { t.Fatal(err) } if longName != "node name" || shortName != "nod" || hwModel != "NODE_HW" || role != "CLIENT" || firmware != "1.2.3" || latitude != 42.5 { t.Fatalf("map_report row = %q/%q/%q/%q firmware %q lat %v, want node fields plus existing map fields", longName, shortName, hwModel, role, firmware, latitude) } } func TestUpsertNodeInfoDoesNotCreateMapReport(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.UpsertNodeInfo(nodeInfoTestRecord("node name")); err != nil { t.Fatalf("UpsertNodeInfo() error = %v", err) } var count int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM map_report WHERE node_id = ?", "!12345678").Scan(&count); err != nil { t.Fatal(err) } if count != 0 { t.Fatalf("map_report count = %d, want 0", count) } } func TestUpsertMapReportUpdatesExistingNodeInfoFields(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.UpsertNodeInfo(nodeInfoTestRecord("node name")); err != nil { t.Fatalf("UpsertNodeInfo() error = %v", err) } report := mapReportTestRecord("map name") report["short_name"] = "map" report["hw_model"] = "MAP_HW" report["role"] = "CLIENT_MUTE" if err := st.UpsertMapReport(report); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } var longName, shortName, hwModel, role, userID, publicKey string if err := rawTestDB(t, st).QueryRow("SELECT long_name, short_name, hw_model, role, user_id, public_key FROM nodeinfo WHERE node_id = ?", "!12345678").Scan(&longName, &shortName, &hwModel, &role, &userID, &publicKey); err != nil { t.Fatal(err) } if longName != "map name" || shortName != "map" || hwModel != "MAP_HW" || role != "CLIENT_MUTE" || userID != "!12345678" || publicKey != "abcd" { t.Fatalf("nodeinfo row = %q/%q/%q/%q user %q key %q, want map fields plus existing node-only fields", longName, shortName, hwModel, role, userID, publicKey) } } func TestUpsertMapReportDoesNotCreateNodeInfo(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.UpsertMapReport(mapReportTestRecord("map name")); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } var count int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM nodeinfo WHERE node_id = ?", "!12345678").Scan(&count); err != nil { t.Fatal(err) } if count != 0 { t.Fatalf("nodeinfo count = %d, want 0", count) } } func TestDeleteNodeDeletesNodeInfoAndMapReport(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.UpsertNodeInfo(nodeInfoTestRecord("node name")); err != nil { t.Fatalf("UpsertNodeInfo() error = %v", err) } if err := st.UpsertMapReport(mapReportTestRecord("map name")); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } if err := st.DeleteNode("!12345678"); err != nil { t.Fatalf("DeleteNode() error = %v", err) } var nodeCount, reportCount int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM nodeinfo WHERE node_id = ?", "!12345678").Scan(&nodeCount); err != nil { t.Fatal(err) } if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM map_report WHERE node_id = ?", "!12345678").Scan(&reportCount); err != nil { t.Fatal(err) } if nodeCount != 0 || reportCount != 0 { t.Fatalf("nodeinfo/map_report counts = %d/%d, want 0/0", nodeCount, reportCount) } if err := st.DeleteNode("!12345678"); !errors.Is(err, gorm.ErrRecordNotFound) { t.Fatalf("DeleteNode(missing) error = %v, want record not found", err) } } func TestUpsertNodeInfoRequiresNodeFields(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.UpsertNodeInfo(map[string]any{"type": "nodeinfo", "from_num": 1}); err == nil || !strings.Contains(err.Error(), "from") { t.Fatalf("missing from error = %v, want from error", err) } if err := st.UpsertNodeInfo(map[string]any{"type": "nodeinfo", "from": "!00000001"}); err == nil || !strings.Contains(err.Error(), "from_num") { t.Fatalf("missing from_num error = %v, want from_num error", err) } } func TestUpsertMapReportRequiresNodeFields(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.UpsertMapReport(map[string]any{"type": "map_report", "from_num": 1}); err == nil || !strings.Contains(err.Error(), "from") { t.Fatalf("missing from error = %v, want from error", err) } if err := st.UpsertMapReport(map[string]any{"type": "map_report", "from": "!00000001"}); err == nil || !strings.Contains(err.Error(), "from_num") { t.Fatalf("missing from_num error = %v, want from_num error", err) } } func TestNodeInfoFromRecordRejectsWrongType(t *testing.T) { _, err := nodeInfoFromRecord(map[string]any{"type": "map_report"}) if err == nil { t.Fatalf("nodeInfoFromRecord() error = nil, want error") } } func TestMapReportFromRecordRejectsWrongType(t *testing.T) { _, err := mapReportFromRecord(map[string]any{"type": "nodeinfo"}) if err == nil { t.Fatalf("mapReportFromRecord() error = nil, want error") } } func TestNodeInfoNullablePublicKey(t *testing.T) { st := openTestStore(t) defer st.Close() record := map[string]any{"type": "nodeinfo", "from": "!00000001", "from_num": 1, "public_key": nil} if err := st.UpsertNodeInfo(record); err != nil { t.Fatalf("UpsertNodeInfo() error = %v", err) } var publicKey sql.NullString if err := rawTestDB(t, st).QueryRow("SELECT public_key FROM nodeinfo WHERE node_id = ?", "!00000001").Scan(&publicKey); err != nil { t.Fatal(err) } if publicKey.Valid { t.Fatalf("public_key valid = true, want null") } } func TestEnsureDefaultAdminCreatesAdminUser(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.EnsureDefaultAdmin("admin", "admin"); err != nil { t.Fatalf("EnsureDefaultAdmin() error = %v", err) } user, err := st.GetUserByUsername("admin") if err != nil { t.Fatalf("GetUserByUsername() error = %v", err) } if user.Role != adminRole { t.Fatalf("role = %q, want admin", user.Role) } if user.PasswordHash == "admin" || user.PasswordHash == "" { t.Fatalf("password hash = %q, want bcrypt hash", user.PasswordHash) } if !verifyPassword(user.PasswordHash, "admin") { t.Fatalf("admin password did not verify") } } func TestEnsureDefaultAdminDoesNotOverwriteExistingUser(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.EnsureDefaultAdmin("admin", "first"); err != nil { t.Fatalf("first EnsureDefaultAdmin() error = %v", err) } if err := st.EnsureDefaultAdmin("admin", "second"); err != nil { t.Fatalf("second EnsureDefaultAdmin() error = %v", err) } user, err := st.GetUserByUsername("admin") if err != nil { t.Fatalf("GetUserByUsername() error = %v", err) } if !verifyPassword(user.PasswordHash, "first") || verifyPassword(user.PasswordHash, "second") { t.Fatalf("admin password was overwritten") } } func TestCreateAdminUserCreatesHashedAdmin(t *testing.T) { st := openTestStore(t) defer st.Close() user, err := st.CreateAdminUser("new-admin", "secret") if err != nil { t.Fatalf("CreateAdminUser() error = %v", err) } if user.Username != "new-admin" || user.Role != adminRole { t.Fatalf("user = %#v, want new-admin admin", user) } if user.PasswordHash == "secret" || !verifyPassword(user.PasswordHash, "secret") { t.Fatalf("password hash did not verify") } } func TestCreateAdminUserRejectsDuplicateUsername(t *testing.T) { st := openTestStore(t) defer st.Close() if _, err := st.CreateAdminUser("new-admin", "secret"); err != nil { t.Fatalf("first CreateAdminUser() error = %v", err) } if _, err := st.CreateAdminUser("new-admin", "secret"); !errors.Is(err, errUserAlreadyExists) { t.Fatalf("duplicate CreateAdminUser() error = %v, want errUserAlreadyExists", err) } } func TestUpdateUserPasswordChangesHash(t *testing.T) { st := openTestStore(t) defer st.Close() user, err := st.CreateAdminUser("new-admin", "old-secret") if err != nil { t.Fatalf("CreateAdminUser() error = %v", err) } oldHash := user.PasswordHash updated, err := st.UpdateUserPassword(user.ID, "new-secret") if err != nil { t.Fatalf("UpdateUserPassword() error = %v", err) } if updated.PasswordHash == oldHash { t.Fatalf("password hash did not change") } if verifyPassword(updated.PasswordHash, "old-secret") || !verifyPassword(updated.PasswordHash, "new-secret") { t.Fatalf("updated password verification mismatch") } } func TestUpdateUserPasswordMissingUser(t *testing.T) { st := openTestStore(t) defer st.Close() if _, err := st.UpdateUserPassword(999, "new-secret"); !errors.Is(err, gorm.ErrRecordNotFound) { t.Fatalf("UpdateUserPassword() error = %v, want record not found", err) } } func TestInsertAndListLoginLogs(t *testing.T) { st := openTestStore(t) defer st.Close() userID := uint64(1) if err := st.InsertLoginLog(loginLogRecord{Username: "admin", UserID: &userID, Success: true, Reason: "success", RemoteAddr: "127.0.0.1:1234", RemoteHost: "127.0.0.1", UserAgent: "test-agent"}); err != nil { t.Fatalf("InsertLoginLog(success) error = %v", err) } if err := st.InsertLoginLog(loginLogRecord{Username: "admin", Success: false, Reason: "invalid username or password", RemoteAddr: "127.0.0.1:1235", RemoteHost: "127.0.0.1", UserAgent: "test-agent"}); err != nil { t.Fatalf("InsertLoginLog(failure) error = %v", err) } logs, err := st.ListLoginLogs(listOptions{Limit: 10}) if err != nil { t.Fatalf("ListLoginLogs() error = %v", err) } if len(logs) != 2 { t.Fatalf("login logs len = %d, want 2", len(logs)) } if logs[0].ID <= logs[1].ID { t.Fatalf("login logs not newest first: ids %d, %d", logs[0].ID, logs[1].ID) } if logs[0].Success || logs[0].Reason != "invalid username or password" { t.Fatalf("latest log = %#v, want failure", logs[0]) } if logs[1].UserID == nil || *logs[1].UserID != userID || !logs[1].Success { t.Fatalf("success log = %#v, want user id and success", logs[1]) } } func TestInsertDiscardDetailsStoresRawBase64AndClientInfo(t *testing.T) { st := openTestStore(t) defer st.Close() raw := []byte{0xff, 0x00, 0x01} clientInfo := mqttClientInfo{ClientID: "client-1", Username: "user-1", Listener: "tcp", RemoteAddr: "127.0.0.1:54321", RemoteHost: "127.0.0.1", RemotePort: "54321"} record := map[string]any{"topic": "msh/US/test", "error": "protobuf decode failed", "payload_len": len(raw)} if err := st.InsertDiscardDetails(record, raw, clientInfo); err != nil { t.Fatalf("InsertDiscardDetails() error = %v", err) } var topic, errorText, rawBase64, clientID, username, listener, remoteAddr, remoteHost, remotePort, contentJSON string var payloadLen int64 if err := rawTestDB(t, st).QueryRow("SELECT topic, error, payload_len, raw_base64, mqtt_client_id, mqtt_username, mqtt_listener, mqtt_remote_addr, mqtt_remote_host, mqtt_remote_port, content_json FROM discard_details LIMIT 1").Scan(&topic, &errorText, &payloadLen, &rawBase64, &clientID, &username, &listener, &remoteAddr, &remoteHost, &remotePort, &contentJSON); err != nil { t.Fatal(err) } if topic != "msh/US/test" || errorText != "protobuf decode failed" || payloadLen != int64(len(raw)) || rawBase64 != base64.StdEncoding.EncodeToString(raw) { t.Fatalf("discard details row = topic %q error %q len %d raw %q", topic, errorText, payloadLen, rawBase64) } if clientID != "client-1" || username != "user-1" || listener != "tcp" || remoteAddr != "127.0.0.1:54321" || remoteHost != "127.0.0.1" || remotePort != "54321" { t.Fatalf("client info = %q %q %q %q %q %q", clientID, username, listener, remoteAddr, remoteHost, remotePort) } if !strings.Contains(contentJSON, "protobuf decode failed") { t.Fatalf("content_json = %q, want error", contentJSON) } } func TestListDiscardDetailsOrdersNewestFirst(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.InsertDiscardDetails(map[string]any{"topic": "first", "error": "first"}, []byte{1}, mqttClientInfo{}); err != nil { t.Fatalf("first InsertDiscardDetails() error = %v", err) } if err := st.InsertDiscardDetails(map[string]any{"topic": "second", "error": "second"}, []byte{2}, mqttClientInfo{}); err != nil { t.Fatalf("second InsertDiscardDetails() error = %v", err) } rows, err := st.ListDiscardDetails(listOptions{Limit: 10}) if err != nil { t.Fatalf("ListDiscardDetails() error = %v", err) } if len(rows) != 2 { t.Fatalf("discard details len = %d, want 2", len(rows)) } if rows[0].ID <= rows[1].ID || rows[0].Topic != "second" { t.Fatalf("discard details order = %#v", rows) } } func TestInsertTextMessageAppendsRows(t *testing.T) { st := openTestStore(t) defer st.Close() clientInfo := mqttClientInfo{ClientID: "client-1", Username: "user-1", Listener: "tcp", RemoteAddr: "127.0.0.1:54321", RemoteHost: "127.0.0.1", RemotePort: "54321"} if err := st.InsertTextMessage(textMessageTestRecord("hello"), clientInfo); err != nil { t.Fatalf("first InsertTextMessage() error = %v", err) } if err := st.InsertTextMessage(textMessageTestRecord("hello again"), clientInfo); err != nil { t.Fatalf("second InsertTextMessage() error = %v", err) } var count int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM text_message WHERE from_id = ?", "!12345678").Scan(&count); err != nil { t.Fatal(err) } if count != 2 { t.Fatalf("text_message count = %d, want 2", count) } rows, err := rawTestDB(t, st).Query("SELECT id FROM text_message ORDER BY id") if err != nil { t.Fatal(err) } defer rows.Close() var ids []int64 for rows.Next() { var id int64 if err := rows.Scan(&id); err != nil { t.Fatal(err) } ids = append(ids, id) } if err := rows.Err(); err != nil { t.Fatal(err) } if len(ids) != 2 || ids[0] <= 0 || ids[1] <= ids[0] { t.Fatalf("ids = %v, want increasing positive ids", ids) } } func TestDeleteTextMessageDeletesRows(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.InsertTextMessage(textMessageTestRecord("hello"), mqttClientInfo{}); err != nil { t.Fatalf("InsertTextMessage() error = %v", err) } var id uint64 if err := rawTestDB(t, st).QueryRow("SELECT id FROM text_message LIMIT 1").Scan(&id); err != nil { t.Fatal(err) } if err := st.DeleteTextMessage(id); err != nil { t.Fatalf("DeleteTextMessage() error = %v", err) } var count int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM text_message WHERE id = ?", id).Scan(&count); err != nil { t.Fatal(err) } if count != 0 { t.Fatalf("text_message count = %d, want 0", count) } if err := st.DeleteTextMessage(id); !errors.Is(err, gorm.ErrRecordNotFound) { t.Fatalf("DeleteTextMessage(missing) error = %v, want record not found", err) } } func TestInsertTextMessageStoresClientInfo(t *testing.T) { st := openTestStore(t) defer st.Close() clientInfo := mqttClientInfo{ClientID: "client-1", Username: "user-1", Listener: "tcp", RemoteAddr: "127.0.0.1:54321", RemoteHost: "127.0.0.1", RemotePort: "54321"} if err := st.InsertTextMessage(textMessageTestRecord("hello"), clientInfo); err != nil { t.Fatalf("InsertTextMessage() error = %v", err) } var clientID, username, listener, remoteAddr, remoteHost, remotePort string if err := rawTestDB(t, st).QueryRow("SELECT mqtt_client_id, mqtt_username, mqtt_listener, mqtt_remote_addr, mqtt_remote_host, mqtt_remote_port FROM text_message LIMIT 1").Scan(&clientID, &username, &listener, &remoteAddr, &remoteHost, &remotePort); err != nil { t.Fatal(err) } if clientID != "client-1" || username != "user-1" || listener != "tcp" || remoteAddr != "127.0.0.1:54321" || remoteHost != "127.0.0.1" || remotePort != "54321" { t.Fatalf("client info = %q %q %q %q %q %q", clientID, username, listener, remoteAddr, remoteHost, remotePort) } } func TestInsertTextMessageStoresPayloadHex(t *testing.T) { st := openTestStore(t) defer st.Close() record := textMessageTestRecord(nil) record["payload_hex"] = "fffefd" if err := st.InsertTextMessage(record, mqttClientInfo{}); err != nil { t.Fatalf("InsertTextMessage() error = %v", err) } var text sql.NullString var payloadHex string if err := rawTestDB(t, st).QueryRow("SELECT text, payload_hex FROM text_message LIMIT 1").Scan(&text, &payloadHex); err != nil { t.Fatal(err) } if text.Valid { t.Fatalf("text valid = true, want null") } if payloadHex != "fffefd" { t.Fatalf("payload_hex = %q, want fffefd", payloadHex) } } func TestInsertTextMessageRequiresFields(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.InsertTextMessage(map[string]any{"type": "nodeinfo"}, mqttClientInfo{}); err == nil || !strings.Contains(err.Error(), "text_message") { t.Fatalf("wrong type error = %v, want text_message error", err) } if err := st.InsertTextMessage(map[string]any{"type": "text_message", "from_num": 1, "topic": "msh/test"}, mqttClientInfo{}); err == nil || !strings.Contains(err.Error(), "from") { t.Fatalf("missing from error = %v, want from error", err) } if err := st.InsertTextMessage(map[string]any{"type": "text_message", "from": "!00000001", "topic": "msh/test"}, mqttClientInfo{}); err == nil || !strings.Contains(err.Error(), "from_num") { t.Fatalf("missing from_num error = %v, want from_num error", err) } if err := st.InsertTextMessage(map[string]any{"type": "text_message", "from": "!00000001", "from_num": 1}, mqttClientInfo{}); err == nil || !strings.Contains(err.Error(), "topic") { t.Fatalf("missing topic error = %v, want topic error", err) } } func TestInsertPositionAppendsRows(t *testing.T) { st := openTestStore(t) defer st.Close() clientInfo := mqttClientInfo{ClientID: "client-1", RemoteAddr: "127.0.0.1:54321", RemoteHost: "127.0.0.1", RemotePort: "54321"} if err := st.InsertPosition(positionTestRecord(), clientInfo); err != nil { t.Fatalf("first InsertPosition() error = %v", err) } if err := st.InsertPosition(positionTestRecord(), clientInfo); err != nil { t.Fatalf("second InsertPosition() error = %v", err) } var count int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM position WHERE from_id = ?", "!12345678").Scan(&count); err != nil { t.Fatal(err) } if count != 2 { t.Fatalf("position count = %d, want 2", count) } var latitude, longitude float64 var altitude int64 var locationSource, remoteHost string if err := rawTestDB(t, st).QueryRow("SELECT latitude, longitude, altitude, location_source, mqtt_remote_host FROM position ORDER BY id LIMIT 1").Scan(&latitude, &longitude, &altitude, &locationSource, &remoteHost); err != nil { t.Fatal(err) } if latitude != 42.5 || longitude != -83.1 || altitude != 200 || locationSource != "LOC_INTERNAL" || remoteHost != "127.0.0.1" { t.Fatalf("position row = lat %v lon %v alt %v source %q remote %q", latitude, longitude, altitude, locationSource, remoteHost) } } func TestInsertPositionCreatesMapReportWhenMissing(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.InsertPosition(positionTestRecord(), mqttClientInfo{}); err != nil { t.Fatalf("InsertPosition() error = %v", err) } var nodeID string var nodeNum int64 var latitude, longitude float64 var altitude, precision int64 if err := rawTestDB(t, st).QueryRow("SELECT node_id, node_num, latitude, longitude, altitude, position_precision FROM map_report WHERE node_id = ?", "!12345678").Scan(&nodeID, &nodeNum, &latitude, &longitude, &altitude, &precision); err != nil { t.Fatal(err) } if nodeID != "!12345678" || nodeNum != 0x12345678 || latitude != 42.5 || longitude != -83.1 || altitude != 200 || precision != 16 { t.Fatalf("map_report from position = %q/%d lat %v lon %v alt %v precision %v", nodeID, nodeNum, latitude, longitude, altitude, precision) } } func TestInsertPositionUpdatesExistingMapReportCoordinates(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.UpsertMapReport(mapReportTestRecord("map name")); err != nil { t.Fatalf("UpsertMapReport() error = %v", err) } position := positionTestRecord() position["latitude"] = 30.25 position["longitude"] = 120.75 position["altitude"] = int32(88) position["precision_bits"] = uint32(10) if err := st.InsertPosition(position, mqttClientInfo{}); err != nil { t.Fatalf("InsertPosition() error = %v", err) } var longName string var latitude, longitude float64 var altitude, precision int64 if err := rawTestDB(t, st).QueryRow("SELECT long_name, latitude, longitude, altitude, position_precision FROM map_report WHERE node_id = ?", "!12345678").Scan(&longName, &latitude, &longitude, &altitude, &precision); err != nil { t.Fatal(err) } if longName != "map name" || latitude != 30.25 || longitude != 120.75 || altitude != 88 || precision != 10 { t.Fatalf("map_report after position = %q lat %v lon %v alt %v precision %v", longName, latitude, longitude, altitude, precision) } } func TestInsertTelemetryAppendsRowsAndStoresMetricsJSON(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.InsertTelemetry(telemetryTestRecord(), mqttClientInfo{}); err != nil { t.Fatalf("InsertTelemetry() error = %v", err) } var telemetryType, metricsJSON, contentJSON string if err := rawTestDB(t, st).QueryRow("SELECT telemetry_type, metrics_json, content_json FROM telemetry LIMIT 1").Scan(&telemetryType, &metricsJSON, &contentJSON); err != nil { t.Fatal(err) } if telemetryType != "device_metrics" { t.Fatalf("telemetry_type = %q, want device_metrics", telemetryType) } if !strings.Contains(metricsJSON, "battery_level") || !strings.Contains(metricsJSON, "voltage") { t.Fatalf("metrics_json = %q, want battery_level and voltage", metricsJSON) } if !strings.Contains(contentJSON, "telemetry") { t.Fatalf("content_json = %q, want telemetry", contentJSON) } } func TestInsertRoutingAndTracerouteAppendRows(t *testing.T) { st := openTestStore(t) defer st.Close() if err := st.InsertRouting(routingTestRecord(), mqttClientInfo{}); err != nil { t.Fatalf("first InsertRouting() error = %v", err) } if err := st.InsertRouting(routingTestRecord(), mqttClientInfo{}); err != nil { t.Fatalf("second InsertRouting() error = %v", err) } if err := st.InsertTraceroute(tracerouteTestRecord(), mqttClientInfo{}); err != nil { t.Fatalf("first InsertTraceroute() error = %v", err) } if err := st.InsertTraceroute(tracerouteTestRecord(), mqttClientInfo{}); err != nil { t.Fatalf("second InsertTraceroute() error = %v", err) } for _, table := range []string{"routing", "traceroute"} { var count int if err := rawTestDB(t, st).QueryRow("SELECT COUNT(*) FROM "+table+" WHERE from_id = ?", "!12345678").Scan(&count); err != nil { t.Fatal(err) } if count != 2 { t.Fatalf("%s count = %d, want 2", table, count) } var packetID int64 var contentJSON string if err := rawTestDB(t, st).QueryRow("SELECT packet_id, content_json FROM "+table+" ORDER BY id LIMIT 1").Scan(&packetID, &contentJSON); err != nil { t.Fatal(err) } if packetID != 42 || !strings.Contains(contentJSON, table) { t.Fatalf("%s row packet_id=%d content_json=%q", table, packetID, contentJSON) } } } func TestInsertPacketTablesRequireFields(t *testing.T) { st := openTestStore(t) defer st.Close() tests := []struct { name string insert func(map[string]any) error record map[string]any }{ {name: "position", insert: func(r map[string]any) error { return st.InsertPosition(r, mqttClientInfo{}) }, record: positionTestRecord()}, {name: "telemetry", insert: func(r map[string]any) error { return st.InsertTelemetry(r, mqttClientInfo{}) }, record: telemetryTestRecord()}, {name: "routing", insert: func(r map[string]any) error { return st.InsertRouting(r, mqttClientInfo{}) }, record: routingTestRecord()}, {name: "traceroute", insert: func(r map[string]any) error { return st.InsertTraceroute(r, mqttClientInfo{}) }, record: tracerouteTestRecord()}, } for _, tt := range tests { wrongType := cloneRecord(tt.record) wrongType["type"] = "text_message" if err := tt.insert(wrongType); err == nil || !strings.Contains(err.Error(), tt.name) { t.Fatalf("%s wrong type error = %v, want %s", tt.name, err, tt.name) } missingFrom := cloneRecord(tt.record) delete(missingFrom, "from") if err := tt.insert(missingFrom); err == nil || !strings.Contains(err.Error(), "from") { t.Fatalf("%s missing from error = %v, want from error", tt.name, err) } missingFromNum := cloneRecord(tt.record) delete(missingFromNum, "from_num") if err := tt.insert(missingFromNum); err == nil || !strings.Contains(err.Error(), "from_num") { t.Fatalf("%s missing from_num error = %v, want from_num error", tt.name, err) } missingTopic := cloneRecord(tt.record) delete(missingTopic, "topic") if err := tt.insert(missingTopic); err == nil || !strings.Contains(err.Error(), "topic") { t.Fatalf("%s missing topic error = %v, want topic error", tt.name, err) } } } func openTestStore(t *testing.T) *store { t.Helper() st, err := openStore(databaseConfig{ Driver: databaseDriverSQLite, SQLite: sqliteConfig{Path: filepath.Join(t.TempDir(), "mesh_mqtt_go.db")}, }) if err != nil { t.Fatalf("openStore() error = %v", err) } return st } func rawTestDB(t *testing.T, st *store) *sql.DB { t.Helper() db, err := st.db.DB() if err != nil { t.Fatalf("st.db.DB() error = %v", err) } return db } func nodeInfoTestRecord(longName string) map[string]any { return map[string]any{ "type": "nodeinfo", "from": "!12345678", "from_num": uint32(0x12345678), "user_id": "!12345678", "long_name": longName, "short_name": "nod", "hw_model": "TEST_HW", "role": "CLIENT", "is_licensed": true, "public_key": "abcd", } } func mapReportTestRecord(longName string) map[string]any { return map[string]any{ "type": "map_report", "from": "!12345678", "from_num": uint32(0x12345678), "long_name": longName, "short_name": "map", "role": "CLIENT_MUTE", "hw_model": "TEST_HW_2", "firmware_version": "1.2.3", "region": "US", "modem_preset": "LONG_FAST", "latitude": 42.5, "longitude": -83.1, "altitude": int32(200), "position_precision": uint32(12), "num_online_local_nodes": uint32(3), "has_opted_report_location": false, } } func textMessageTestRecord(text any) map[string]any { record := commonPacketTestRecord("text_message", "TEXT_MESSAGE_APP") record["text"] = text return record } func positionTestRecord() map[string]any { record := commonPacketTestRecord("position", "POSITION_APP") record["latitude"] = 42.5 record["longitude"] = -83.1 record["altitude"] = int32(200) record["time"] = uint32(123456) record["location_source"] = "LOC_INTERNAL" record["altitude_source"] = "ALT_INTERNAL" record["timestamp"] = uint32(123456) record["timestamp_millis_adjust"] = uint32(10) record["altitude_hae"] = int32(210) record["altitude_geoidal_separation"] = int32(20) record["pdop"] = 1.1 record["hdop"] = 1.2 record["vdop"] = 1.3 record["gps_accuracy"] = uint32(1000) record["ground_speed"] = uint32(2) record["ground_track"] = 180.5 record["fix_quality"] = uint32(1) record["fix_type"] = uint32(3) record["sats_in_view"] = uint32(8) record["sensor_id"] = uint32(1) record["next_update"] = uint32(60) record["seq_number"] = uint32(7) record["precision_bits"] = uint32(16) return record } func telemetryTestRecord() map[string]any { record := commonPacketTestRecord("telemetry", "TELEMETRY_APP") record["time"] = uint32(123456) record["telemetry_type"] = "device_metrics" record["metrics"] = map[string]any{"battery_level": 85, "voltage": 4.1} return record } func routingTestRecord() map[string]any { return commonPacketTestRecord("routing", "ROUTING_APP") } func tracerouteTestRecord() map[string]any { return commonPacketTestRecord("traceroute", "TRACEROUTE_APP") } func commonPacketTestRecord(recordType, portnum string) map[string]any { return map[string]any{ "type": recordType, "topic": "msh/US/test", "channel_id": "LongFast", "gateway_id": "!gateway", "from": "!12345678", "from_num": uint32(0x12345678), "packet_id": uint32(42), "packet_to": "!ffffffff", "packet_to_num": uint32(0xffffffff), "portnum": portnum, "payload_len": 5, "payload_variant": "decoded", "via_mqtt": true, "pki_encrypted": false, } } func cloneRecord(record map[string]any) map[string]any { clone := make(map[string]any, len(record)) for key, value := range record { clone[key] = value } return clone }