tailscale/control/tsp/nodefile_test.go
Brad Fitzpatrick 50d7176333 control/tsp, cmd/tsp: add low-level Tailscale protocol client and tool
Add a new control/tsp package providing a client for speaking the
Tailscale protocol to a coordination server over Noise, along with a
cmd/tsp binary exposing it as a low-level composable tool for
generating keys, registering nodes, and issuing map requests.

Previously developed out-of-tree at github.com/bradfitz/tsp; imported
here without git history.

Updates #12542

Change-Id: I6ad21143c4aefe8939d4a46ae65b2184173bf69f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-04-16 20:00:25 -07:00

117 lines
2.9 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tsp
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"tailscale.com/types/key"
)
func TestNodeFileRoundTrip(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "node.json")
nf := NodeFile{
NodeKey: key.NewNode(),
MachineKey: key.NewMachine(),
ServerInfo: ServerInfo{
URL: "https://controlplane.tailscale.com",
Key: key.NewMachine().Public(),
},
}
if err := WriteNodeFile(path, nf); err != nil {
t.Fatalf("WriteNodeFile: %v", err)
}
got, err := ReadNodeFile(path)
if err != nil {
t.Fatalf("ReadNodeFile: %v", err)
}
if !got.NodeKey.Equal(nf.NodeKey) {
t.Errorf("node key mismatch")
}
if !got.MachineKey.Equal(nf.MachineKey) {
t.Errorf("machine key mismatch")
}
if got.URL != nf.URL {
t.Errorf("server URL = %q, want %q", got.URL, nf.URL)
}
if got.ServerInfo.Key != nf.ServerInfo.Key {
t.Errorf("server key mismatch")
}
}
// TestNodeFileFormat verifies that ReadNodeFile can parse a fixed JSON literal,
// ensuring we don't accidentally change the on-disk format.
func TestNodeFileFormat(t *testing.T) {
const fileContents = `{
"node_key": "privkey:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"machine_key": "privkey:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
"server_url": "https://controlplane.tailscale.com",
"server_key": "mkey:1111111111111111111111111111111111111111111111111111111111111111"
}`
dir := t.TempDir()
path := filepath.Join(dir, "node.json")
if err := os.WriteFile(path, []byte(fileContents), 0600); err != nil {
t.Fatal(err)
}
nf, err := ReadNodeFile(path)
if err != nil {
t.Fatalf("ReadNodeFile: %v", err)
}
if nf.NodeKey.IsZero() {
t.Error("node key is zero")
}
if nf.MachineKey.IsZero() {
t.Error("machine key is zero")
}
if nf.URL != "https://controlplane.tailscale.com" {
t.Errorf("server URL = %q", nf.URL)
}
if nf.ServerInfo.Key.IsZero() {
t.Error("server key is zero")
}
}
// TestNodeFileWriteFormat verifies that WriteNodeFile produces the expected
// JSON field names.
func TestNodeFileWriteFormat(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "node.json")
nf := NodeFile{
NodeKey: key.NewNode(),
MachineKey: key.NewMachine(),
ServerInfo: ServerInfo{
URL: "https://example.com",
Key: key.NewMachine().Public(),
},
}
if err := WriteNodeFile(path, nf); err != nil {
t.Fatalf("WriteNodeFile: %v", err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatalf("parsing written JSON: %v", err)
}
for _, field := range []string{"node_key", "machine_key", "server_url", "server_key"} {
if _, ok := raw[field]; !ok {
t.Errorf("missing JSON field %q in written file", field)
}
}
}