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.
This commit is contained in:
parent
ce8707c618
commit
b521af3364
2 changed files with 61 additions and 2 deletions
|
|
@ -38,6 +38,20 @@ function buildCopyCommand(text, clipboardCmd) {
|
||||||
return ["sh", "-c", "printf '%s' '" + escaped + "' | " + 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) {
|
function buildToggleCommand(isConnected) {
|
||||||
return isConnected ? ["tailscale", "down"] : ["tailscale", "up"]
|
return isConnected ? ["tailscale", "down"] : ["tailscale", "up"]
|
||||||
}
|
}
|
||||||
|
|
@ -61,5 +75,5 @@ function errorMessage(cmd) {
|
||||||
|
|
||||||
// CommonJS export for Node.js tests (ignored by QML)
|
// CommonJS export for Node.js tests (ignored by QML)
|
||||||
if (typeof module !== "undefined" && module.exports) {
|
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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { test } from "node:test"
|
import { test } from "node:test"
|
||||||
import assert from "node:assert"
|
import assert from "node:assert"
|
||||||
import lib from "../tailscalectl/lib.js"
|
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", () => {
|
test("parsePeers extracts exitNode from ExitNodeOption", () => {
|
||||||
const peerMap = {
|
const peerMap = {
|
||||||
|
|
@ -158,3 +158,48 @@ test("buildToggleCommand treats null and undefined as disconnected", () => {
|
||||||
assert.deepStrictEqual(buildToggleCommand(null), ["tailscale", "up"])
|
assert.deepStrictEqual(buildToggleCommand(null), ["tailscale", "up"])
|
||||||
assert.deepStrictEqual(buildToggleCommand(undefined), ["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)
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue