From b521af3364cb6576ca5882614d9c4ee8e2287ff0 Mon Sep 17 00:00:00 2001 From: vybe Date: Wed, 20 May 2026 19:29:02 +0000 Subject: [PATCH] feat: add parseStatusResult for safe JSON parsing of tailscale status Wraps JSON.parse in try/catch, returns safe defaults on failure, and delegates to existing findActiveExitNode and parsePeers. --- tailscalectl/lib.js | 16 ++++++++++++++- test/lib.test.js | 47 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/tailscalectl/lib.js b/tailscalectl/lib.js index 563de83..6260d5b 100644 --- a/tailscalectl/lib.js +++ b/tailscalectl/lib.js @@ -38,6 +38,20 @@ function buildCopyCommand(text, clipboardCmd) { return ["sh", "-c", "printf '%s' '" + escaped + "' | " + clipboardCmd] } +function parseStatusResult(jsonText) { + try { + const data = JSON.parse(jsonText) + return { + isConnected: data.BackendState === "Running", + tailscaleIP: (data.Self && data.Self.TailscaleIPs && data.Self.TailscaleIPs[0]) || "", + currentExitNode: findActiveExitNode(data.Peer || {}), + peers: parsePeers(data.Peer || {}) + } + } catch (e) { + return { isConnected: false, tailscaleIP: "", currentExitNode: "", peers: [] } + } +} + function buildToggleCommand(isConnected) { return isConnected ? ["tailscale", "down"] : ["tailscale", "up"] } @@ -61,5 +75,5 @@ function errorMessage(cmd) { // CommonJS export for Node.js tests (ignored by QML) if (typeof module !== "undefined" && module.exports) { - module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand, parseClipboardDetection, buildToggleCommand } + module.exports = { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand, parseClipboardDetection, buildToggleCommand, parseStatusResult } } diff --git a/test/lib.test.js b/test/lib.test.js index c5b5098..c43d596 100644 --- a/test/lib.test.js +++ b/test/lib.test.js @@ -1,7 +1,7 @@ import { test } from "node:test" import assert from "node:assert" import lib from "../tailscalectl/lib.js" -const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand, parseClipboardDetection, buildToggleCommand } = lib +const { parsePeers, makeExitNodeCommand, findActiveExitNode, errorMessage, validateClipboardCmd, buildCopyCommand, parseClipboardDetection, buildToggleCommand, parseStatusResult } = lib test("parsePeers extracts exitNode from ExitNodeOption", () => { const peerMap = { @@ -158,3 +158,48 @@ test("buildToggleCommand treats null and undefined as disconnected", () => { assert.deepStrictEqual(buildToggleCommand(null), ["tailscale", "up"]) assert.deepStrictEqual(buildToggleCommand(undefined), ["tailscale", "up"]) }) + +// --- parseStatusResult --- + +test("parseStatusResult produces correct state from valid JSON", () => { + const json = JSON.stringify({ + BackendState: "Running", + Self: { TailscaleIPs: ["100.64.0.5"] }, + Peer: { + "key-1": { HostName: "router", TailscaleIPs: ["100.64.0.1"], Online: true, ExitNode: true, ExitNodeOption: true } + } + }) + const state = parseStatusResult(json) + assert.strictEqual(state.isConnected, true) + assert.strictEqual(state.tailscaleIP, "100.64.0.5") + assert.strictEqual(state.currentExitNode, "router") + assert.strictEqual(state.peers.length, 1) +}) + +test("parseStatusResult returns safe defaults for invalid JSON", () => { + const state = parseStatusResult("not json at all") + assert.strictEqual(state.isConnected, false) + assert.strictEqual(state.tailscaleIP, "") + assert.strictEqual(state.currentExitNode, "") + assert.strictEqual(state.peers.length, 0) +}) + +test("parseStatusResult handles missing Self gracefully", () => { + const json = JSON.stringify({ BackendState: "Running", Peer: {} }) + const state = parseStatusResult(json) + assert.strictEqual(state.isConnected, true) + assert.strictEqual(state.tailscaleIP, "") +}) + +test("parseStatusResult handles missing and empty Peer gracefully", () => { + const json = JSON.stringify({ BackendState: "Running", Self: { TailscaleIPs: ["100.64.0.5"] } }) + const state = parseStatusResult(json) + assert.strictEqual(state.peers.length, 0) + assert.strictEqual(state.currentExitNode, "") +}) + +test("parseStatusResult sets isConnected false for non-Running BackendState", () => { + const json = JSON.stringify({ BackendState: "NeedsLogin", Self: { TailscaleIPs: ["100.64.0.5"] }, Peer: {} }) + const state = parseStatusResult(json) + assert.strictEqual(state.isConnected, false) +})