mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-11 16:54:58 +00:00
b7c280c20a
## Summary Closes #871 The `/api/packets` endpoint could return packets with `null` hash or timestamp fields. This was caused by legacy data in SQLite (rows with empty `hash` or `NULL`/empty `first_seen`) predating the ingestor's existing validation guard (`if hash == "" { return false, nil }` at `cmd/ingestor/db.go:610`). ## Root Cause `cmd/server/store.go` `filterPackets()` had no data-integrity guard. Legacy rows with empty `hash` or `first_seen` were loaded into the in-memory store and returned verbatim. The `strOrNil("")` helper then serialized these as JSON `null`. ## Fix Added a data-integrity predicate at the top of `filterPackets`'s scan callback (`cmd/server/store.go:2278`): ```go if tx.Hash == "" || tx.FirstSeen == "" { return false } ``` This filters bad legacy rows at query time. The write path (ingestor) already rejects empty hashes, so no new bad data enters. ## TDD Evidence - **Red commit:** `15774c3` — test `TestIssue871_NoNullHashOrTimestamp` asserts no packet in API response has null/empty hash or timestamp - **Green commit:** `281fd6f` — adds the filter guard, test passes ## Testing - `go test ./...` in `cmd/server` passes (full suite) - Client-side defensive filter from PR #868 remains as defense-in-depth --------- Co-authored-by: you <you@example.com>
64 lines
1.8 KiB
Go
64 lines
1.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// TestIssue871_NoNullHashOrTimestamp verifies that /api/packets never returns
|
|
// packets with null/empty hash or null timestamp (issue #871).
|
|
func TestIssue871_NoNullHashOrTimestamp(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
seedTestData(t, db)
|
|
|
|
// Insert bad legacy data: packet with empty hash
|
|
now := time.Now().UTC().Add(-30 * time.Minute).Format(time.RFC3339)
|
|
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
|
|
VALUES ('DEAD', '', ?, 1, 4, '{}')`, now)
|
|
// Insert bad legacy data: packet with NULL first_seen (timestamp)
|
|
db.conn.Exec(`INSERT INTO transmissions (raw_hex, hash, first_seen, route_type, payload_type, decoded_json)
|
|
VALUES ('BEEF', 'aa11bb22cc33dd44', NULL, 1, 4, '{}')`)
|
|
|
|
cfg := &Config{Port: 3000}
|
|
hub := NewHub()
|
|
srv := NewServer(db, cfg, hub)
|
|
store := NewPacketStore(db, nil)
|
|
if err := store.Load(); err != nil {
|
|
t.Fatalf("store.Load failed: %v", err)
|
|
}
|
|
srv.store = store
|
|
router := mux.NewRouter()
|
|
srv.RegisterRoutes(router)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/packets?limit=200", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var resp struct {
|
|
Packets []map[string]interface{} `json:"packets"`
|
|
}
|
|
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
|
t.Fatalf("decode error: %v", err)
|
|
}
|
|
|
|
for i, p := range resp.Packets {
|
|
hash, _ := p["hash"]
|
|
ts, _ := p["timestamp"]
|
|
if hash == nil || hash == "" {
|
|
t.Errorf("packet[%d] has null/empty hash: %v", i, p)
|
|
}
|
|
if ts == nil || ts == "" {
|
|
t.Errorf("packet[%d] has null/empty timestamp: %v", i, p)
|
|
}
|
|
}
|
|
}
|