mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-04-22 20:15:42 +00:00
## Problem Channel API endpoints scan entire DB — 2.4s for channel list, 30s for messages. ## Fix - Added `channel_hash` column to transmissions (populated on ingest, backfilled on startup) - `GetChannels()` rewrites to GROUP BY channel_hash (one row per channel vs scanning every packet) - `GetChannelMessages()` filters by channel_hash at SQL level with proper LIMIT/OFFSET - 60s cache for channel list - Index: `idx_tx_channel_hash` for fast lookups Expected: 2.4s → <100ms for list, 30s → <500ms for messages. Fixes #762 --------- Co-authored-by: you <you@example.com>
146 lines
4.7 KiB
Go
146 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// seedEncryptedChannelData adds undecryptable GRP_TXT packets to the test DB.
|
|
func seedEncryptedChannelData(t *testing.T, db *DB) {
|
|
t.Helper()
|
|
now := time.Now().UTC()
|
|
recent := now.Add(-1 * time.Hour).Format(time.RFC3339)
|
|
recentEpoch := now.Add(-1 * time.Hour).Unix()
|
|
|
|
// Two encrypted GRP_TXT packets on channel hash "A1B2"
|
|
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json, channel_hash)
|
|
VALUES ('EE01', 'enc_hash_001', ?, 1, 5, '{"type":"GRP_TXT","channelHashHex":"A1B2","decryptionStatus":"no_key"}', 'enc_A1B2')`, recent)
|
|
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json, channel_hash)
|
|
VALUES ('EE02', 'enc_hash_002', ?, 1, 5, '{"type":"GRP_TXT","channelHashHex":"A1B2","decryptionStatus":"no_key"}', 'enc_A1B2')`, recent)
|
|
|
|
// Observations for both
|
|
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES ((SELECT id FROM transmissions WHERE hash='enc_hash_001'), 1, 10.0, -90, '[]', ?)`, recentEpoch)
|
|
db.conn.Exec(`INSERT INTO observations (transmission_id, observer_idx, snr, rssi, path_json, timestamp)
|
|
VALUES ((SELECT id FROM transmissions WHERE hash='enc_hash_002'), 1, 10.0, -90, '[]', ?)`, recentEpoch)
|
|
}
|
|
|
|
func TestGetEncryptedChannels(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
seedTestData(t, db)
|
|
seedEncryptedChannelData(t, db)
|
|
|
|
channels, err := db.GetEncryptedChannels()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(channels) != 1 {
|
|
t.Fatalf("expected 1 encrypted channel, got %d", len(channels))
|
|
}
|
|
ch := channels[0]
|
|
if ch["hash"] != "enc_A1B2" {
|
|
t.Errorf("expected hash enc_A1B2, got %v", ch["hash"])
|
|
}
|
|
if ch["encrypted"] != true {
|
|
t.Errorf("expected encrypted=true, got %v", ch["encrypted"])
|
|
}
|
|
if ch["messageCount"] != 2 {
|
|
t.Errorf("expected messageCount=2, got %v", ch["messageCount"])
|
|
}
|
|
}
|
|
|
|
func TestChannelsAPIExcludesEncrypted(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
// Seed encrypted data into the server's DB
|
|
// setupTestServer uses seedTestData which has no encrypted packets,
|
|
// so default /api/channels should NOT include encrypted channels.
|
|
req := httptest.NewRequest("GET", "/api/channels", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
channels := body["channels"].([]interface{})
|
|
|
|
for _, ch := range channels {
|
|
m := ch.(map[string]interface{})
|
|
if enc, ok := m["encrypted"]; ok && enc == true {
|
|
t.Errorf("default /api/channels should not include encrypted channels, found: %v", m["hash"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChannelsAPIIncludesEncryptedWithParam(t *testing.T) {
|
|
srv, router := setupTestServer(t)
|
|
// Add encrypted data to the server's DB
|
|
seedEncryptedChannelData(t, srv.db)
|
|
// Reload store so in-memory also has the data
|
|
store := NewPacketStore(srv.db, nil)
|
|
if err := store.Load(); err != nil {
|
|
t.Fatalf("store.Load: %v", err)
|
|
}
|
|
srv.store = store
|
|
|
|
req := httptest.NewRequest("GET", "/api/channels?includeEncrypted=true", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
channels := body["channels"].([]interface{})
|
|
|
|
foundEncrypted := false
|
|
for _, ch := range channels {
|
|
m := ch.(map[string]interface{})
|
|
if enc, ok := m["encrypted"]; ok && enc == true {
|
|
foundEncrypted = true
|
|
break
|
|
}
|
|
}
|
|
if !foundEncrypted {
|
|
t.Error("expected encrypted channels with includeEncrypted=true, found none")
|
|
}
|
|
}
|
|
|
|
func TestChannelMessagesExcludesEncrypted(t *testing.T) {
|
|
srv, router := setupTestServer(t)
|
|
seedEncryptedChannelData(t, srv.db)
|
|
store := NewPacketStore(srv.db, nil)
|
|
if err := store.Load(); err != nil {
|
|
t.Fatalf("store.Load: %v", err)
|
|
}
|
|
srv.store = store
|
|
|
|
// Request messages for the encrypted channel — should return empty
|
|
req := httptest.NewRequest("GET", "/api/channels/enc_A1B2/messages", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
var body map[string]interface{}
|
|
json.Unmarshal(w.Body.Bytes(), &body)
|
|
messages, ok := body["messages"].([]interface{})
|
|
if !ok {
|
|
// messages might be null/missing — that's fine, means no messages
|
|
return
|
|
}
|
|
// Encrypted messages should not be returned as readable messages
|
|
for _, msg := range messages {
|
|
m := msg.(map[string]interface{})
|
|
if text, ok := m["text"].(string); ok && text != "" {
|
|
t.Errorf("encrypted channel should not return readable messages, got text: %s", text)
|
|
}
|
|
}
|
|
}
|