The default stays v2. A reflector that refuses that version and names the other gets one session of it, then the station announces the configured protocol again. Co-authored-by: Cursor <cursoragent@cursor.com>
684 lines
24 KiB
Go
684 lines
24 KiB
Go
package station_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
jsonv2 "encoding/json/v2"
|
|
|
|
"gitea.neitzel.de/konrad/funknetz/station"
|
|
)
|
|
|
|
func TestMissingConfigOpensWithoutCreating(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), ".funknetz.json")
|
|
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
if _, statErr := os.Stat(path); !os.IsNotExist(statErr) {
|
|
t.Fatalf("stat = %v, want the file absent", statErr)
|
|
}
|
|
|
|
got := st.State()
|
|
if !got.NeedsConfig || !got.ConfigOpen || got.Connection != "Disconnected" {
|
|
t.Fatalf("State = %+v, want Disconnected with the Config window open", got)
|
|
}
|
|
cfg := st.Config()
|
|
if !cfg.Open || !cfg.NeedsConfig || cfg.Error != "" {
|
|
t.Fatalf("Config = %+v, want an open needs-config form and no error", cfg)
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
form := cfg.Form
|
|
if form.Host != "" || form.Callsign != "" || form.AuthKey != "" {
|
|
t.Fatalf("string fields = %+v, want empty host, callsign, and authKey", form)
|
|
}
|
|
if form.Port != 0 || form.TalkGroup != 0 {
|
|
t.Fatalf("numeric fields = %+v, want port and talkGroup at zero", form)
|
|
}
|
|
if form.EnableLogging {
|
|
t.Fatal("EnableLogging = true, want false")
|
|
}
|
|
if form.Protocol != "v2" {
|
|
t.Fatalf("Protocol = %q, want v2", form.Protocol)
|
|
}
|
|
if form.CertificateDir != "./funknetz" {
|
|
t.Fatalf("CertificateDir = %q, want ./funknetz", form.CertificateDir)
|
|
}
|
|
if form.LogFile != filepath.Join(home, "funknetz.log") {
|
|
t.Fatalf("LogFile = %q, want funknetz.log in the home directory", form.LogFile)
|
|
}
|
|
}
|
|
|
|
func TestExistingConfigIsNotRewritten(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
original := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"protocol\":\"v2\",\"certificateDir\":\"/var/funknetz\",\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n")
|
|
if err := os.WriteFile(path, original, 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := station.New(station.Options{ConfigPath: path}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != string(original) {
|
|
t.Fatalf("file = %q, want %q", got, original)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := info.Mode().Perm(), os.FileMode(0o640); got != want {
|
|
t.Fatalf("mode = %o, want %o", got, want)
|
|
}
|
|
}
|
|
|
|
func TestInvalidConfigNamesTheField(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
host string
|
|
port int
|
|
callsign string
|
|
authKey string
|
|
talkGroup int
|
|
body string
|
|
want string
|
|
}{
|
|
{name: "empty host", host: "", port: 5308, callsign: "DL1ABC", authKey: "key", talkGroup: 1, want: "host is empty"},
|
|
{name: "blank host", host: " ", port: 5308, callsign: "DL1ABC", authKey: "key", talkGroup: 1, want: "host is empty"},
|
|
{name: "host type", body: `{"host":1,"port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":1}`, want: "host has the wrong type"},
|
|
{name: "empty callsign", host: "127.0.0.1", port: 5308, callsign: "", authKey: "key", talkGroup: 1, want: "callsign is empty"},
|
|
{name: "callsign type", body: `{"host":"127.0.0.1","port":5308,"callsign":1,"authKey":"key","talkGroup":1}`, want: "callsign has the wrong type"},
|
|
{name: "empty auth key", host: "127.0.0.1", port: 5308, callsign: "DL1ABC", authKey: "", talkGroup: 1, want: "authKey is empty"},
|
|
{name: "whitespace auth key", host: "127.0.0.1", port: 5308, callsign: "DL1ABC", authKey: " \t", talkGroup: 1, want: "authKey is empty"},
|
|
{name: "auth key type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":1,"talkGroup":1}`, want: "authKey has the wrong type"},
|
|
{name: "port zero", host: "127.0.0.1", port: 0, callsign: "DL1ABC", authKey: "key", talkGroup: 1, want: "port is invalid"},
|
|
{name: "port negative", host: "127.0.0.1", port: -1, callsign: "DL1ABC", authKey: "key", talkGroup: 1, want: "port is invalid"},
|
|
{name: "port too high", host: "127.0.0.1", port: 65536, callsign: "DL1ABC", authKey: "key", talkGroup: 1, want: "port is invalid"},
|
|
{name: "port type", body: `{"host":"127.0.0.1","port":"5308","callsign":"DL1ABC","authKey":"key","talkGroup":1}`, want: "port has the wrong type"},
|
|
{name: "port fraction", body: `{"host":"127.0.0.1","port":5308.5,"callsign":"DL1ABC","authKey":"key","talkGroup":1}`, want: "port is invalid"},
|
|
{name: "talk group zero", host: "127.0.0.1", port: 5308, callsign: "DL1ABC", authKey: "key", talkGroup: 0, want: "talkGroup is invalid"},
|
|
{name: "talk group negative", host: "127.0.0.1", port: 5308, callsign: "DL1ABC", authKey: "key", talkGroup: -3, want: "talkGroup is invalid"},
|
|
{name: "talk group type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":"20"}`, want: "talkGroup has the wrong type"},
|
|
{name: "bad callsign", host: "127.0.0.1", port: 5308, callsign: "HELLO", authKey: "key", talkGroup: 1, want: "callsign is invalid"},
|
|
{name: "protocol type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":1,"protocol":2}`, want: "protocol has the wrong type"},
|
|
{name: "protocol value", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":1,"protocol":"v1"}`, want: "protocol is invalid"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
body := tt.body
|
|
if body == "" {
|
|
body = configJSON(t, tt.host, tt.port, tt.callsign, tt.authKey, tt.talkGroup)
|
|
}
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
state := st.State()
|
|
if !state.NeedsConfig || !state.ConfigOpen || state.Detail != tt.want {
|
|
t.Fatalf("State = %+v, want the Config window open with detail %q", state, tt.want)
|
|
}
|
|
if st.Config().Error != tt.want {
|
|
t.Fatalf("Config error = %q, want %q", st.Config().Error, tt.want)
|
|
}
|
|
after, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(tt.want, "wrong type") {
|
|
if string(after) != string(before) {
|
|
t.Fatalf("file = %q, want it left untouched", after)
|
|
}
|
|
return
|
|
}
|
|
var got map[string]any
|
|
if err := jsonv2.Unmarshal(after, &got); err != nil {
|
|
t.Fatalf("rewritten file is not JSON: %v\n%s", err, after)
|
|
}
|
|
if got["enableLogging"] != false {
|
|
t.Fatalf("enableLogging = %#v, want false", got["enableLogging"])
|
|
}
|
|
if _, ok := got["logFile"].(string); !ok {
|
|
t.Fatalf("logFile = %#v, want a string", got["logFile"])
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBrokenFileSalvagesTheOtherFields(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
body := `{"host":1,"port":5308,"callsign":"DL1ABC","authKey":"a b","talkGroup":20,"certificateDir":"./radios","enableLogging":true,"logFile":"/tmp/funknetz.log"}`
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
form := st.Config().Form
|
|
if form.Host != "" || form.Port != 5308 || form.Callsign != "DL1ABC" || form.AuthKey != "a b" || form.TalkGroup != 20 {
|
|
t.Fatalf("Form = %+v, want the valid fields kept and host cleared", form)
|
|
}
|
|
if form.CertificateDir != "./radios" || !form.EnableLogging || form.LogFile != "/tmp/funknetz.log" {
|
|
t.Fatalf("Form = %+v, want certificate, logging, and log file kept", form)
|
|
}
|
|
after, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(after) != body {
|
|
t.Fatalf("file = %s, want it left untouched", after)
|
|
}
|
|
}
|
|
|
|
func TestBrokenConfigSaysTheFileCouldNotBeRead(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
if err := os.WriteFile(path, []byte("{"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
got := st.Config()
|
|
if !got.Open || !got.NeedsConfig {
|
|
t.Fatalf("Config = %+v, want the Config window open", got)
|
|
}
|
|
if !strings.Contains(got.Error, "the file could not be read") {
|
|
t.Fatalf("Config error = %q, want it to say the file could not be read", got.Error)
|
|
}
|
|
if strings.Contains(got.Error, "callsign") {
|
|
t.Fatalf("Config error = %q, want no field name", got.Error)
|
|
}
|
|
if got.Form.Host != "" || got.Form.Callsign != "" || got.Form.Port != 0 || got.Form.CertificateDir != "./funknetz" {
|
|
t.Fatalf("Form = %+v, want template defaults", got.Form)
|
|
}
|
|
}
|
|
|
|
func TestCallsignIsTrimmedAndUppercased(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
body := configJSON(t, " 127.0.0.1 ", 5308, " dl1abc-r ", "a b", 20)
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := st.State()
|
|
if got.Callsign != "DL1ABC-R" {
|
|
t.Fatalf("Callsign = %q, want DL1ABC-R", got.Callsign)
|
|
}
|
|
if got.TalkGroup != 20 {
|
|
t.Fatalf("TalkGroup = %d, want 20", got.TalkGroup)
|
|
}
|
|
if got.Connection != "Disconnected" || got.Detail != "Idle" || got.Talker != "" || got.Transmitting {
|
|
t.Fatalf("State = %+v, want Disconnected, Idle, no talker", got)
|
|
}
|
|
}
|
|
|
|
func TestMissingLogKeysAreRewritten(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
original := []byte("{\"note\":true,\"host\":\" 127.0.0.1 \",\"port\":5308,\"callsign\":\" dl1abc \",\"authKey\":\"a b\",\"talkGroup\":20}\n")
|
|
if err := os.WriteFile(path, original, 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
if st.State().Callsign != "DL1ABC" {
|
|
t.Fatalf("Callsign = %q", st.State().Callsign)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := info.Mode().Perm(), os.FileMode(0o640); got != want {
|
|
t.Fatalf("mode = %o, want %o", got, want)
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got map[string]any
|
|
if err := jsonv2.Unmarshal(body, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, ok := got["note"]; ok {
|
|
t.Fatalf("note = %#v, want it dropped", got["note"])
|
|
}
|
|
if got["callsign"] != " dl1abc " || got["authKey"] != "a b" || got["host"] != " 127.0.0.1 " {
|
|
t.Fatalf("kept fields = %#v", got)
|
|
}
|
|
if got["enableLogging"] != false {
|
|
t.Fatalf("enableLogging = %#v", got["enableLogging"])
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["logFile"] != filepath.Join(home, "funknetz.log") {
|
|
t.Fatalf("logFile = %#v", got["logFile"])
|
|
}
|
|
if got["protocol"] != "v2" {
|
|
t.Fatalf("protocol = %#v, want v2", got["protocol"])
|
|
}
|
|
if got["certificateDir"] != "./funknetz" {
|
|
t.Fatalf("certificateDir = %#v, want ./funknetz", got["certificateDir"])
|
|
}
|
|
assertKeyOrder(t, body)
|
|
if st.State().Detail != "Idle" {
|
|
t.Fatalf("Detail = %q, want Idle", st.State().Detail)
|
|
}
|
|
}
|
|
|
|
func TestLogSettingsDoNotStopTheStation(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
|
|
t.Run("missing log file uses home", func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
body := []byte(`{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":true}`)
|
|
if err := os.WriteFile(path, body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fields map[string]any
|
|
if err := jsonv2.Unmarshal(got, &fields); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantPath := filepath.Join(home, "funknetz.log")
|
|
if fields["logFile"] != wantPath || fields["enableLogging"] != true {
|
|
t.Fatalf("fields = %#v", fields)
|
|
}
|
|
if _, err := os.Stat(wantPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
|
|
t.Run("logging off ignores a bad path", func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
logPath := filepath.Join(t.TempDir(), "funknetz.log")
|
|
body := []byte(`{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":false,"logFile":1}`)
|
|
if err := os.WriteFile(path, body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
if st.State().Detail != "Idle" {
|
|
t.Fatalf("Detail = %q, want Idle", st.State().Detail)
|
|
}
|
|
after, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(after) != string(before) {
|
|
t.Fatalf("file = %s, want it left untouched", after)
|
|
}
|
|
if _, err := os.Stat(logPath); !os.IsNotExist(err) {
|
|
t.Fatalf("log file err = %v, want not exist", err)
|
|
}
|
|
})
|
|
|
|
cases := []struct {
|
|
name string
|
|
body string
|
|
detail string
|
|
same bool
|
|
}{
|
|
{name: "relative", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"protocol":"v2","certificateDir":"./funknetz","enableLogging":true,"logFile":"funknetz.log"}`, detail: "Cannot write funknetz.log", same: true},
|
|
{name: "empty", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"protocol":"v2","certificateDir":"./funknetz","enableLogging":true,"logFile":" "}`, detail: "Cannot write logFile", same: true},
|
|
{name: "tilde", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"protocol":"v2","certificateDir":"./funknetz","enableLogging":true,"logFile":"~/funknetz.log"}`, detail: "Cannot write ~/funknetz.log", same: true},
|
|
{name: "enable type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":"yes","logFile":"/tmp/funknetz.log"}`, detail: "enableLogging has the wrong type", same: true},
|
|
{name: "file type", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":true,"logFile":1}`, detail: "logFile has the wrong type", same: true},
|
|
{name: "both types", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":"yes","logFile":1}`, detail: "enableLogging has the wrong type", same: true},
|
|
{name: "missing enable and bad file", body: `{"host":"127.0.0.1","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"logFile":1}`, detail: "Idle", same: true},
|
|
}
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
if err := os.WriteFile(path, []byte(tt.body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
if st.State().Detail != tt.detail {
|
|
t.Fatalf("Detail = %q, want %q", st.State().Detail, tt.detail)
|
|
}
|
|
if tt.same {
|
|
after, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(after) != tt.body {
|
|
t.Fatalf("file = %s, want it left untouched", after)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("bad host opens the config window", func(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
body := []byte(`{"host":"","port":5308,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"enableLogging":"yes"}`)
|
|
if err := os.WriteFile(path, body, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
got := st.State()
|
|
if !got.NeedsConfig || !got.ConfigOpen || got.Detail != "host is empty" {
|
|
t.Fatalf("State = %+v, want the Config window open with host is empty", got)
|
|
}
|
|
after, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(after) != string(body) {
|
|
t.Fatalf("file = %s, want it left untouched", after)
|
|
}
|
|
})
|
|
}
|
|
|
|
func assertKeyOrder(t *testing.T, body []byte) {
|
|
t.Helper()
|
|
keys := []string{`"host"`, `"port"`, `"callsign"`, `"authKey"`, `"talkGroup"`, `"protocol"`, `"certificateDir"`, `"enableLogging"`, `"logFile"`}
|
|
last := -1
|
|
for _, key := range keys {
|
|
i := strings.Index(string(body), key)
|
|
if i < 0 || i < last {
|
|
t.Fatalf("key %s out of order in %s", key, body)
|
|
}
|
|
last = i
|
|
}
|
|
}
|
|
|
|
func TestInvalidSaveWritesNothingAndDoesNotConnect(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
go func() {
|
|
// Run's error is observed through State, which is the station seam.
|
|
_ = st.Run(ctx)
|
|
}()
|
|
|
|
err = st.Save(station.ConfigForm{
|
|
Host: "",
|
|
Port: port,
|
|
Callsign: "DL1ABC",
|
|
AuthKey: "key",
|
|
TalkGroup: 20,
|
|
CertificateDir: "./funknetz",
|
|
LogFile: "/tmp/funknetz.log",
|
|
})
|
|
if err == nil || err.Error() != "host is empty" {
|
|
t.Fatalf("Save() error = %v, want host is empty", err)
|
|
}
|
|
if _, statErr := os.Stat(path); !os.IsNotExist(statErr) {
|
|
t.Fatalf("stat = %v, want the file absent", statErr)
|
|
}
|
|
cfg := st.Config()
|
|
if !cfg.Open || cfg.Error != "host is empty" || cfg.Form.Callsign != "DL1ABC" {
|
|
t.Fatalf("Config = %+v, want the window open with the host error and the edited callsign", cfg)
|
|
}
|
|
waitCtx, waitCancel := context.WithTimeout(t.Context(), 200*time.Millisecond)
|
|
defer waitCancel()
|
|
if _, err := st.Wait(waitCtx, func(s station.State) bool { return s.Connection != "Disconnected" }); err == nil {
|
|
t.Fatalf("State = %+v, want it to stay Disconnected", st.State())
|
|
}
|
|
ln.(*net.TCPListener).SetDeadline(time.Now().Add(200 * time.Millisecond))
|
|
if conn, err := ln.Accept(); err == nil {
|
|
conn.Close()
|
|
t.Fatal("opened a socket")
|
|
}
|
|
}
|
|
|
|
func TestValidSaveCreatesFileAndConnects(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "funknetz.json")
|
|
logFile := filepath.Join(dir, "funknetz.log")
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
go func() {
|
|
// Run's error is observed through State, which is the station seam.
|
|
_ = st.Run(ctx)
|
|
}()
|
|
|
|
err = st.Save(station.ConfigForm{
|
|
Host: " 127.0.0.1 ",
|
|
Port: port,
|
|
Callsign: " dl1abc ",
|
|
AuthKey: "a b",
|
|
TalkGroup: 20,
|
|
CertificateDir: "./funknetz",
|
|
LogFile: logFile,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st.Config().Open || st.State().NeedsConfig {
|
|
t.Fatalf("State = %+v, Config open = %v, want the Config window closed", st.State(), st.Config().Open)
|
|
}
|
|
if st.State().Callsign != "DL1ABC" || st.State().TalkGroup != 20 {
|
|
t.Fatalf("State = %+v, want callsign DL1ABC talk group 20", st.State())
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := info.Mode().Perm(), os.FileMode(0o600); got != want {
|
|
t.Fatalf("mode = %o, want %o", got, want)
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := fmt.Sprintf("{\"host\":\"127.0.0.1\",\"port\":%d,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"protocol\":\"v2\",\"certificateDir\":\"./funknetz\",\"enableLogging\":false,\"logFile\":%q}\n", port, logFile)
|
|
if string(body) != want {
|
|
t.Fatalf("file = %s, want %s", body, want)
|
|
}
|
|
assertKeyOrder(t, body)
|
|
ln.(*net.TCPListener).SetDeadline(time.Now().Add(2 * time.Second))
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn.Close()
|
|
}
|
|
|
|
func TestMissingCertificateDirIsWrittenAfterTalkGroup(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
original := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n")
|
|
if err := os.WriteFile(path, original, 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := station.New(station.Options{ConfigPath: path}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := info.Mode().Perm(), os.FileMode(0o640); got != want {
|
|
t.Fatalf("mode = %o, want %o", got, want)
|
|
}
|
|
body, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var got map[string]any
|
|
if err := jsonv2.Unmarshal(body, &got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["authKey"] != "a b" || got["callsign"] != "DL1ABC" || got["certificateDir"] != "./funknetz" || got["protocol"] != "v2" {
|
|
t.Fatalf("fields = %#v", got)
|
|
}
|
|
if strings.Contains(string(body), t.TempDir()) {
|
|
t.Fatalf("file = %s, want the stored directory to stay relative", body)
|
|
}
|
|
assertKeyOrder(t, body)
|
|
}
|
|
|
|
func TestCertificateDirStopsBeforeASocket(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
dir string
|
|
want string
|
|
}{
|
|
{name: "wrong type", dir: "1", want: "certificateDir has the wrong type"},
|
|
{name: "empty", dir: `""`, want: "certificateDir is empty"},
|
|
{name: "whitespace", dir: `" "`, want: "certificateDir is empty"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
body := fmt.Sprintf(`{"host":"127.0.0.1","port":%d,"callsign":"DL1ABC","authKey":"key","talkGroup":20,"protocol":"v2","certificateDir":%s,"enableLogging":false,"logFile":"/tmp/funknetz.log"}`, port, tt.dir)
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
got := st.State()
|
|
if got.Detail != tt.want || got.Connection != "Disconnected" || got.Callsign != "DL1ABC" {
|
|
t.Fatalf("State = %+v, want detail %q", got, tt.want)
|
|
}
|
|
after, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(after) != body {
|
|
t.Fatalf("file = %s, want it left untouched", after)
|
|
}
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
go func() {
|
|
// Run's error is observed through State, which is the station seam.
|
|
_ = st.Run(ctx)
|
|
}()
|
|
ln.(*net.TCPListener).SetDeadline(time.Now().Add(200 * time.Millisecond))
|
|
if conn, err := ln.Accept(); err == nil {
|
|
conn.Close()
|
|
t.Fatal("opened a socket")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func configJSON(t testing.TB, host string, port int, callsign, authKey string, talkGroup int) string {
|
|
t.Helper()
|
|
return configJSONProtocol(t, host, port, callsign, authKey, talkGroup, "")
|
|
}
|
|
|
|
func configJSONProtocol(t testing.TB, host string, port int, callsign, authKey string, talkGroup int, protocol string) string {
|
|
t.Helper()
|
|
fields := map[string]any{
|
|
"host": host,
|
|
"port": port,
|
|
"callsign": callsign,
|
|
"authKey": authKey,
|
|
"talkGroup": talkGroup,
|
|
}
|
|
if protocol != "" {
|
|
fields["protocol"] = protocol
|
|
}
|
|
body, err := jsonv2.Marshal(fields)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(body)
|
|
}
|
|
|
|
func configWithLog(t testing.TB, host string, port int, callsign, authKey string, talkGroup int, logPath string) string {
|
|
t.Helper()
|
|
body, err := jsonv2.Marshal(map[string]any{
|
|
"host": host,
|
|
"port": port,
|
|
"callsign": callsign,
|
|
"authKey": authKey,
|
|
"talkGroup": talkGroup,
|
|
"enableLogging": true,
|
|
"logFile": logPath,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(body)
|
|
}
|