mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-12 21:54:48 +00:00
df69a17718
## Summary Fixes #772 — adds a short-URL form for node detail pages so operators can paste node links into a mesh chat without bringing along a 64-hex-char public key. ## Approach **Pubkey-prefix resolution** (no allocator, no lookup table). - The SPA hash route `#/nodes/<key>` already accepts whatever pubkey-shaped string the user pastes; the front end forwards it to `GET /api/nodes/<key>`. - When that lookup misses **and** the path is 8..63 hex chars, the backend now calls `DB.GetNodeByPrefix` and: - returns the matching node when exactly one node has that prefix, - returns **409 Conflict** when multiple nodes share the prefix (with a "use a longer prefix" hint), - falls through to the existing 404 otherwise. - 8 hex chars = 32 bits of entropy, which is enough for fleets in the low thousands. Operators can extend to 10–12 chars if collisions become common. - The full-screen node detail card gets a new **📡 Copy short URL** button that copies `…/#/nodes/<first 8 hex chars>`. ### Why not an opaque ID table (`/s/<id>`)? Considered and rejected: - Needs persistence + an allocator + cleanup story. - IDs aren't self-describing — operators can't sanity-check them. - IDs don't survive a DB rebuild. - 32 bits of pubkey already buys us collision resistance with zero moving parts. If the directory grows past the point where 8-char prefixes routinely collide, we can extend the minimum length without changing the URL shape. ## Changes - `cmd/server/db.go` — new `GetNodeByPrefix(prefix)` returning `(node, ambiguous, error)`. Validates hex; rejects <8 chars; `LIMIT 2` to detect collisions cheaply. - `cmd/server/routes.go` — `handleNodeDetail` falls back to prefix resolution; canonicalizes pubkey downstream; emits 409 on ambiguity; honors blacklist on the resolved pubkey. - `public/nodes.js` — adds **📡 Copy short URL** button + handler on the full-screen node detail card. - `cmd/server/short_url_test.go` — Go tests (red-then-green). - `test-e2e-playwright.js` — E2E: navigates via prefix-only URL and asserts the new button surfaces. ## TDD evidence - Red commit: `2dea97a` — tests added with a stub `GetNodeByPrefix` returning `(nil, false, nil)`. All four assertions failed (assertion failures, not build errors): expected node got nil; expected ambiguous=true got false; route 404 vs expected 200/409. - Green commit: `9b8f146` — implementation lands; `go test ./...` passes locally in `cmd/server`. ## Compatibility - Existing 64-char pubkey URLs are untouched (exact lookup runs first). - Blacklist is enforced both on the raw input and on the resolved pubkey. - No new config knobs. ## What I did **not** touch - `cmd/server/db_test.go`, other route tests — unchanged. - Packet-detail short URLs (issue scopes nodes; revisit in a follow-up if asked). Fixes #772 --------- Co-authored-by: clawbot <bot@corescope.local>
110 lines
3.3 KiB
Go
110 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// Issue #772 — shortened URL for easier sending over the mesh.
|
|
//
|
|
// Public keys are 64 hex chars. Operators want to share node URLs over a
|
|
// mesh radio link where every byte counts. We allow truncating the pubkey
|
|
// in the URL down to a minimum 8-hex-char prefix; the server resolves the
|
|
// prefix back to the full pubkey when (and only when) it is unambiguous.
|
|
|
|
func TestResolveNodePrefix_Unique(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
seedTestData(t, db)
|
|
|
|
// "aabbccdd" uniquely identifies the seeded TestRepeater (pubkey aabbccdd11223344).
|
|
node, ambiguous, err := db.GetNodeByPrefix("aabbccdd")
|
|
if err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if ambiguous {
|
|
t.Fatalf("expected unambiguous match, got ambiguous=true")
|
|
}
|
|
if node == nil {
|
|
t.Fatalf("expected node, got nil")
|
|
}
|
|
if got, _ := node["public_key"].(string); got != "aabbccdd11223344" {
|
|
t.Errorf("expected public_key aabbccdd11223344, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestResolveNodePrefix_Ambiguous(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
seedTestData(t, db)
|
|
|
|
// Insert a second node sharing the 8-char prefix "aabbccdd".
|
|
if _, err := db.conn.Exec(`INSERT INTO nodes (public_key, name, role, advert_count)
|
|
VALUES ('aabbccdd99887766', 'OtherNode', 'companion', 1)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
node, ambiguous, err := db.GetNodeByPrefix("aabbccdd")
|
|
if err != nil {
|
|
t.Fatalf("unexpected err: %v", err)
|
|
}
|
|
if !ambiguous {
|
|
t.Fatalf("expected ambiguous=true for shared prefix, got false (node=%v)", node)
|
|
}
|
|
if node != nil {
|
|
t.Errorf("expected nil node when ambiguous, got %v", node["public_key"])
|
|
}
|
|
}
|
|
|
|
func TestResolveNodePrefix_TooShort(t *testing.T) {
|
|
db := setupTestDB(t)
|
|
defer db.Close()
|
|
seedTestData(t, db)
|
|
|
|
// <8 hex chars must NOT resolve, even if it would be unique.
|
|
node, _, err := db.GetNodeByPrefix("aabbccd")
|
|
if err == nil && node != nil {
|
|
t.Errorf("expected nil/error for 7-char prefix, got node %v", node["public_key"])
|
|
}
|
|
}
|
|
|
|
// Route-level: GET /api/nodes/<8-char-prefix> resolves to the full node.
|
|
func TestNodeDetailRoute_PrefixResolves(t *testing.T) {
|
|
_, router := setupTestServer(t)
|
|
|
|
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 for unique 8-char prefix, got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
var body NodeDetailResponse
|
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
pk, _ := body.Node["public_key"].(string)
|
|
if pk != "aabbccdd11223344" {
|
|
t.Errorf("expected resolved pubkey aabbccdd11223344, got %q", pk)
|
|
}
|
|
}
|
|
|
|
// Route-level: GET /api/nodes/<ambiguous-prefix> returns 409 with a hint.
|
|
func TestNodeDetailRoute_PrefixAmbiguous(t *testing.T) {
|
|
srv, router := setupTestServer(t)
|
|
if _, err := srv.db.conn.Exec(`INSERT INTO nodes (public_key, name, role, advert_count)
|
|
VALUES ('aabbccdd99887766', 'OtherNode', 'companion', 1)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/nodes/aabbccdd", nil)
|
|
w := httptest.NewRecorder()
|
|
router.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusConflict {
|
|
t.Fatalf("expected 409 for ambiguous prefix, got %d body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|