//go:build !bindings package main import ( "bytes" "context" "os" "path/filepath" "strings" "testing" ) func TestResolveConfigPathDefaultIsHome(t *testing.T) { got, err := resolveConfigPath(nil) if err != nil { t.Fatal(err) } home, err := os.UserHomeDir() if err != nil { t.Fatal(err) } want := filepath.Join(home, ".funknetz.json") if got != want { t.Fatalf("resolveConfigPath(nil) = %q, want %q", got, want) } } func TestResolveConfigPathLongAndShort(t *testing.T) { dir := t.TempDir() abs := filepath.Join(dir, "lab.json") got, err := resolveConfigPath([]string{"--config", abs}) if err != nil { t.Fatal(err) } if got != abs { t.Fatalf("--config = %q, want %q", got, abs) } got, err = resolveConfigPath([]string{"-c", abs}) if err != nil { t.Fatal(err) } if got != abs { t.Fatalf("-c = %q, want %q", got, abs) } } func TestResolveConfigPathRelativeBecomesAbsolute(t *testing.T) { dir := t.TempDir() cwd, err := os.Getwd() if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Chdir(cwd) }) if err := os.Chdir(dir); err != nil { t.Fatal(err) } got, err := resolveConfigPath([]string{"-c", "./funknetz.json"}) if err != nil { t.Fatal(err) } want, err := filepath.EvalSymlinks(filepath.Join(dir, "funknetz.json")) if err != nil { // File need not exist; resolve the directory and join. base, err := filepath.EvalSymlinks(dir) if err != nil { t.Fatal(err) } want = filepath.Join(base, "funknetz.json") } gotEval, err := filepath.EvalSymlinks(filepath.Dir(got)) if err != nil { t.Fatal(err) } got = filepath.Join(gotEval, filepath.Base(got)) if got != want { t.Fatalf("relative -c = %q, want %q", got, want) } } func TestOpenStationUsesResolvedPath(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "custom.json") st, err := openStation([]string{"-c", path}) if err != nil { t.Fatal(err) } if _, statErr := os.Stat(path); !os.IsNotExist(statErr) { t.Fatalf("stat = %v, want the file absent until Save", statErr) } if !st.State().NeedsConfig || !st.State().ConfigOpen { t.Fatalf("State = %+v, want the Config window open", st.State()) } // Same rules as the home file: a second open with a filled config works. body := `{ "host": "127.0.0.1", "port": 5300, "callsign": "DL0FN-1", "authKey": "local-lab-secret-change-me", "talkGroup": 9, "enableLogging": false, "logFile": "` + filepath.Join(dir, "funknetz.log") + `" } ` if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } st, err = openStation([]string{"--config", path}) if err != nil { t.Fatal(err) } if st.State().Callsign != "DL0FN-1" { t.Fatalf("callsign = %q, want DL0FN-1", st.State().Callsign) } } func TestConfigEqualsFlagSelectsTheFile(t *testing.T) { abs := filepath.Join(t.TempDir(), "lab.json") got, err := resolveConfigPath([]string{"--config=" + abs}) if err != nil { t.Fatal(err) } if got != abs { t.Fatalf("--config= = %q, want %q", got, abs) } } func TestMissingFileOpensWithoutStderr(t *testing.T) { path := filepath.Join(t.TempDir(), "lab.json") var stderr bytes.Buffer st, err := openStationReported([]string{"-c", path}, &stderr) if err != nil { t.Fatal(err) } if st == nil { t.Fatal("station = nil, want a station so the window can open") } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } if _, statErr := os.Stat(path); !os.IsNotExist(statErr) { t.Fatalf("stat = %v, want the file absent", statErr) } if !st.State().NeedsConfig || !st.State().ConfigOpen { t.Fatalf("State = %+v, want the Config window open", st.State()) } form := st.Config().Form if form.Host != "" || form.Callsign != "" || form.AuthKey != "" || form.Port != 0 || form.TalkGroup != 0 || form.EnableLogging || form.CertificateDir != "./funknetz" { t.Fatalf("Form = %+v, want template defaults", form) } home, err := os.UserHomeDir() if err != nil { t.Fatal(err) } if form.LogFile != filepath.Join(home, "funknetz.log") { t.Fatalf("LogFile = %q, want funknetz.log in the home directory", form.LogFile) } } func TestLaterFlagSelectsTheFile(t *testing.T) { dir := t.TempDir() first := filepath.Join(dir, "first.json") second := filepath.Join(dir, "second.json") got, err := resolveConfigPath([]string{"-c", first, "--config", second}) if err != nil { t.Fatal(err) } if got != second { t.Fatalf("later --config = %q, want %q", got, second) } got, err = resolveConfigPath([]string{"--config", first, "-c", second}) if err != nil { t.Fatal(err) } if got != second { t.Fatalf("later -c = %q, want %q", got, second) } } func TestEmptyConfigFlagUsesHomeFile(t *testing.T) { home, err := os.UserHomeDir() if err != nil { t.Fatal(err) } want := filepath.Join(home, ".funknetz.json") for _, args := range [][]string{ {"-c", ""}, {"--config", ""}, {"--config="}, } { got, err := resolveConfigPath(args) if err != nil { t.Fatal(err) } if got != want { t.Fatalf("resolveConfigPath(%q) = %q, want %q", args, got, want) } } } func TestExtraWordsDoNotChangeTheSelectedFile(t *testing.T) { abs := filepath.Join(t.TempDir(), "lab.json") got, err := resolveConfigPath([]string{"-c", abs, "leftover"}) if err != nil { t.Fatal(err) } if got != abs { t.Fatalf("with leftover = %q, want %q", got, abs) } home, err := os.UserHomeDir() if err != nil { t.Fatal(err) } got, err = resolveConfigPath([]string{"note"}) if err != nil { t.Fatal(err) } want := filepath.Join(home, ".funknetz.json") if got != want { t.Fatalf("bare word = %q, want %q", got, want) } got, err = resolveConfigPath([]string{"note", "-c", abs}) if err != nil { t.Fatal(err) } if got != abs { t.Fatalf("word before -c = %q, want %q", got, abs) } second := filepath.Join(filepath.Dir(abs), "second.json") got, err = resolveConfigPath([]string{"-c", abs, "leftover", "--config", second}) if err != nil { t.Fatal(err) } if got != second { t.Fatalf("flag after a word = %q, want %q", got, second) } } func TestSelectedFileIsLeftByteForByte(t *testing.T) { path := filepath.Join(t.TempDir(), "lab.json") original := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"a b\",\"talkGroup\":20,\"protocol\":\"v2\",\"certificateDir\":\"./funknetz\",\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n") if err := os.WriteFile(path, original, 0o640); err != nil { t.Fatal(err) } st, err := openStation([]string{"--config", path}) if err != nil { t.Fatal(err) } if st.State().Callsign != "DL1ABC" { t.Fatalf("callsign = %q, want DL1ABC", st.State().Callsign) } got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if string(got) != string(original) { t.Fatalf("file = %q, want %q", got, original) } } func TestEmptyHostOnSelectedFileOpensConfig(t *testing.T) { path := filepath.Join(t.TempDir(), "lab.json") body := []byte("{\"host\":\"\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"key\",\"talkGroup\":20,\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n") if err := os.WriteFile(path, body, 0o600); err != nil { t.Fatal(err) } var stderr bytes.Buffer st, err := openStationReported([]string{"-c", path}, &stderr) if err != nil { t.Fatal(err) } if st == nil || !st.State().ConfigOpen || !strings.Contains(st.State().Detail, "host") { t.Fatalf("State = %+v, want the Config window open naming host", st.State()) } if stderr.Len() != 0 { t.Fatalf("stderr = %q, want empty", stderr.String()) } } func TestCancelWithoutALoadedConfigQuits(t *testing.T) { path := filepath.Join(t.TempDir(), "missing.json") st, err := openStation([]string{"-c", path}) if err != nil { t.Fatal(err) } quit := false app := &App{ station: st, ctx: t.Context(), quit: func(context.Context) { quit = true }, } app.CancelConfig() if !quit { t.Fatal("CancelConfig() left the app running") } } func TestCancelAfterLoadKeepsTheSession(t *testing.T) { path := filepath.Join(t.TempDir(), "lab.json") body := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"key\",\"talkGroup\":20,\"protocol\":\"v2\",\"certificateDir\":\"./funknetz\",\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n") if err := os.WriteFile(path, body, 0o600); err != nil { t.Fatal(err) } st, err := openStation([]string{"-c", path}) if err != nil { t.Fatal(err) } quit := false app := &App{ station: st, ctx: t.Context(), quit: func(context.Context) { quit = true }, } app.CancelConfig() if quit { t.Fatal("CancelConfig() quit after a loaded config") } if st.State().ConfigOpen { t.Fatal("Config window stayed open") } got, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if string(got) != string(body) { t.Fatalf("file = %s, want it left unchanged", got) } } func TestBadFlagPrintsParserError(t *testing.T) { cases := []struct { args []string want string }{ {[]string{"-not-a-flag"}, "flag provided but not defined: -not-a-flag\n"}, {[]string{"-c"}, "flag needs an argument: -c\n"}, {[]string{"--config"}, "flag needs an argument: -config\n"}, } for _, tc := range cases { var stderr bytes.Buffer st, err := openStationReported(tc.args, &stderr) if st != nil { t.Fatal("station = non-nil, want nil") } if err == nil { t.Fatal("error = nil, want a flag error") } if got := stderr.String(); got != tc.want { t.Fatalf("stderr = %q, want %q", got, tc.want) } if strings.Contains(strings.ToLower(stderr.String()), "usage") { t.Fatalf("stderr = %q, want no usage text", stderr.String()) } } }