A log path that cannot be written leaves the station on the reflector, and the reflector learns which program joined. Co-authored-by: Cursor <cursoragent@cursor.com>
391 lines
14 KiB
Go
391 lines
14 KiB
Go
package station_test
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
jsonv2 "encoding/json/v2"
|
|
|
|
"gitea.neitzel.de/konrad/funknetz/station"
|
|
)
|
|
|
|
func TestMissingConfigIsCreatedAndRejected(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), ".funknetz.json")
|
|
|
|
_, err := station.New(station.Options{ConfigPath: path})
|
|
if err == nil {
|
|
t.Fatal("New() error = nil, want a rejection after creating the file")
|
|
}
|
|
if !strings.Contains(err.Error(), "created") {
|
|
t.Fatalf("New() error = %q, want it to say the file was created", err.Error())
|
|
}
|
|
|
|
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)
|
|
}
|
|
var got map[string]any
|
|
if err := jsonv2.Unmarshal(body, &got); err != nil {
|
|
t.Fatalf("created file is not JSON: %v\n%s", err, body)
|
|
}
|
|
if got["host"] != "" || got["callsign"] != "" || got["authKey"] != "" {
|
|
t.Fatalf("string fields = %#v, want empty host, callsign, and authKey", got)
|
|
}
|
|
if got["port"] != float64(0) || got["talkGroup"] != float64(0) {
|
|
t.Fatalf("numeric fields = %#v, want port and talkGroup at zero", got)
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got["enableLogging"] != false {
|
|
t.Fatalf("enableLogging = %#v, want false", got["enableLogging"])
|
|
}
|
|
if got["logFile"] != filepath.Join(home, "funknetz.log") {
|
|
t.Fatalf("logFile = %#v, want funknetz.log in the home directory", got["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,\"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
|
|
body string
|
|
want string
|
|
}{
|
|
{name: "empty host", body: configJSON("", 5308, "DL1ABC", "key", 1), want: "host is empty"},
|
|
{name: "blank host", body: configJSON(" ", 5308, "DL1ABC", "key", 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", body: configJSON("127.0.0.1", 5308, "", "key", 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", body: configJSON("127.0.0.1", 5308, "DL1ABC", "", 1), want: "authKey is empty"},
|
|
{name: "whitespace auth key", body: configJSON("127.0.0.1", 5308, "DL1ABC", " \t", 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", body: configJSON("127.0.0.1", 0, "DL1ABC", "key", 1), want: "port is invalid"},
|
|
{name: "port negative", body: configJSON("127.0.0.1", -1, "DL1ABC", "key", 1), want: "port is invalid"},
|
|
{name: "port too high", body: configJSON("127.0.0.1", 65536, "DL1ABC", "key", 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", body: configJSON("127.0.0.1", 5308, "DL1ABC", "key", 0), want: "talkGroup is invalid"},
|
|
{name: "talk group negative", body: configJSON("127.0.0.1", 5308, "DL1ABC", "key", -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", body: configJSON("127.0.0.1", 5308, "HELLO", "key", 1), want: "callsign is invalid"},
|
|
}
|
|
for _, tt := range tests {
|
|
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)
|
|
}
|
|
before, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = station.New(station.Options{ConfigPath: path})
|
|
if err == nil {
|
|
t.Fatal("New() error = nil, want a field rejection")
|
|
}
|
|
if err.Error() != tt.want {
|
|
t.Fatalf("New() error = %q, want %q", err.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 TestBrokenConfigSaysTheFileCouldNotBeRead(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
if err := os.WriteFile(path, []byte("{"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err := station.New(station.Options{ConfigPath: path})
|
|
if err == nil {
|
|
t.Fatal("New() error = nil, want a read failure")
|
|
}
|
|
if err.Error() != "could not be read" {
|
|
t.Fatalf("New() error = %q, want %q", err.Error(), "could not be read")
|
|
}
|
|
if strings.Contains(err.Error(), "callsign") {
|
|
t.Fatalf("New() error = %q, want no field name", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestCallsignIsTrimmedAndUppercased(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "funknetz.json")
|
|
body := configJSON(" 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 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,"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,"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,"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 still quits", 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)
|
|
}
|
|
_, err := station.New(station.Options{ConfigPath: path})
|
|
if err == nil || err.Error() != "host is empty" {
|
|
t.Fatalf("New() error = %v, want host is empty", err)
|
|
}
|
|
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 configJSON(host string, port int, callsign, authKey string, talkGroup int) string {
|
|
body, err := jsonv2.Marshal(map[string]any{
|
|
"host": host,
|
|
"port": port,
|
|
"callsign": callsign,
|
|
"authKey": authKey,
|
|
"talkGroup": talkGroup,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(body)
|
|
}
|
|
|
|
func configWithLog(host string, port int, callsign, authKey string, talkGroup int, logPath string) string {
|
|
body, err := jsonv2.Marshal(map[string]any{
|
|
"host": host,
|
|
"port": port,
|
|
"callsign": callsign,
|
|
"authKey": authKey,
|
|
"talkGroup": talkGroup,
|
|
"enableLogging": true,
|
|
"logFile": logPath,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return string(body)
|
|
}
|