715 lines
24 KiB
Go
715 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"database/sql"
|
|
"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", "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 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 != "node name" || userID != "!12345678" || publicKey != "abcd" {
|
|
t.Fatalf("nodeinfo row = %q/%q/%q, want node 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 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 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 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
|
|
}
|