508 lines
15 KiB
Go
508 lines
15 KiB
Go
package station_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.neitzel.de/konrad/funknetz/station"
|
|
)
|
|
|
|
func TestOpenConfigRereadsTheFile(t *testing.T) {
|
|
path := writeConfigFile(t, fullConfig("127.0.0.1", 5308, "DL1ABC", "key", 20, "/tmp/funknetz.log"))
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
edited := fullConfig("127.0.0.1", 5308, "DL1ABD", "key", 21, "/tmp/funknetz.log")
|
|
if err := os.WriteFile(path, []byte(edited), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st.OpenConfig()
|
|
cfg := st.Config()
|
|
if !cfg.Open || cfg.Error != "" || cfg.Form.Callsign != "DL1ABD" || cfg.Form.TalkGroup != 21 {
|
|
t.Fatalf("Config = %+v, want the hand-edited callsign and talk group", cfg)
|
|
}
|
|
if st.State().Callsign != "DL1ABC" || st.State().TalkGroup != 20 {
|
|
t.Fatalf("State = %+v, want the session to keep the loaded config", st.State())
|
|
}
|
|
}
|
|
|
|
func TestFailedRereadKeepsTheSession(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
if err := os.WriteFile(in.path, []byte("{"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.st.OpenConfig()
|
|
cfg := in.st.Config()
|
|
if !cfg.Open || !strings.Contains(cfg.Error, "the file could not be read") {
|
|
t.Fatalf("Config = %+v, want an open window and a read error", cfg)
|
|
}
|
|
got := in.st.State()
|
|
if got.Connection != "Connected" || got.Callsign != "DL1ABC" || got.TalkGroup != 20 {
|
|
t.Fatalf("State = %+v, want the live session kept", got)
|
|
}
|
|
if err := connStillOpen(in.refl.conn()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCancelAfterLoadLeavesFileAndSession(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
before, err := os.ReadFile(in.path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.st.OpenConfig()
|
|
outside := []byte(fullConfig("127.0.0.1", in.refl.port, "DL1ABD", "other", 21, "/tmp/funknetz.log"))
|
|
if err := os.WriteFile(in.path, outside, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if in.st.Cancel() {
|
|
t.Fatal("Cancel() quit after a loaded config")
|
|
}
|
|
if in.st.State().ConfigOpen || in.st.State().Connection != "Connected" || in.st.State().Callsign != "DL1ABC" {
|
|
t.Fatalf("State = %+v, want the window closed and the session kept", in.st.State())
|
|
}
|
|
got, err := os.ReadFile(in.path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != string(outside) {
|
|
t.Fatalf("file = %s, want the outside edit left in place", got)
|
|
}
|
|
if string(before) == string(got) {
|
|
t.Fatal("file did not change, so the test did not prove Cancel left it alone")
|
|
}
|
|
if err := connStillOpen(in.refl.conn()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSaveRewritesAndReconnects(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
if err := os.Chmod(in.path, 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.st.OpenConfig()
|
|
form := in.st.Config().Form
|
|
form.TalkGroup = 21
|
|
if err := in.st.Save(form); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if in.st.Config().Open {
|
|
t.Fatal("Config window stayed open")
|
|
}
|
|
info, err := os.Stat(in.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(in.path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(body), `"talkGroup":21`) || !strings.HasSuffix(string(body), "\n") {
|
|
t.Fatalf("file = %s, want talk group 21 and a trailing newline", body)
|
|
}
|
|
assertKeyOrder(t, body)
|
|
|
|
in.refl.accept(t)
|
|
declineTo2(t, in.refl)
|
|
challenge := make([]byte, 20)
|
|
for i := range challenge {
|
|
challenge[i] = byte(i + 1)
|
|
}
|
|
in.refl.writeFrame(t, 10, appendBytes(nil, challenge))
|
|
typ, _ := in.refl.readFrame(t)
|
|
if typ != 11 {
|
|
t.Fatalf("auth type %d, want 11", typ)
|
|
}
|
|
in.refl.writeFrame(t, 12, nil)
|
|
in.refl.writeFrame(t, 100, serverInfo(7, []string{"db0abc"}, []string{"OPUS"}))
|
|
typ, _ = in.refl.readFrame(t)
|
|
if typ != 111 {
|
|
t.Fatalf("type %d, want software identity", typ)
|
|
}
|
|
typ, tgBody := in.refl.readFrame(t)
|
|
if typ != 106 {
|
|
t.Fatalf("type %d, want select talk group", typ)
|
|
}
|
|
gotTG, _ := takeU32(t, tgBody)
|
|
if gotTG != 21 {
|
|
t.Fatalf("talk group %d, want 21", gotTG)
|
|
}
|
|
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
|
|
defer cancel()
|
|
state, err := in.st.Wait(ctx, func(s station.State) bool { return s.Connection == "Connected" && s.TalkGroup == 21 })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if state.Callsign != "DL1ABC" {
|
|
t.Fatalf("Callsign = %q, want DL1ABC", state.Callsign)
|
|
}
|
|
}
|
|
|
|
func TestInvalidSaveLeavesTheSession(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
before, err := os.ReadFile(in.path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.st.OpenConfig()
|
|
form := in.st.Config().Form
|
|
form.Host = ""
|
|
err = in.st.Save(form)
|
|
if err == nil || err.Error() != "host is empty" {
|
|
t.Fatalf("Save() error = %v, want host is empty", err)
|
|
}
|
|
got, err := os.ReadFile(in.path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != string(before) {
|
|
t.Fatalf("file = %s, want it unchanged", got)
|
|
}
|
|
state := in.st.State()
|
|
if !state.ConfigOpen || state.Connection != "Connected" || state.Callsign != "DL1ABC" || state.Detail != "host is empty" {
|
|
t.Fatalf("State = %+v, want the window open, the session up, and the host error", state)
|
|
}
|
|
if err := connStillOpen(in.refl.conn()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestPushToTalkIsOffWhileConfigIsOpen(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
in.st.OpenConfig()
|
|
in.st.HoldButton()
|
|
if in.st.State().Transmitting || in.st.State().Connection != "Connected" {
|
|
t.Fatalf("State = %+v, want listening with push-to-talk off", in.st.State())
|
|
}
|
|
if err := connStillOpen(in.refl.conn()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestConfigStaysClosedWhilePushToTalkIsHeld(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
in.st.HoldButton()
|
|
if !in.st.State().Transmitting {
|
|
t.Fatal("Transmitting = false, want the button held")
|
|
}
|
|
in.st.OpenConfig()
|
|
state := in.st.State()
|
|
if state.ConfigOpen || state.Detail != "Release push-to-talk" || !state.Transmitting {
|
|
t.Fatalf("State = %+v, want no Config window and a push-to-talk notice", state)
|
|
}
|
|
in.st.ReleaseButton()
|
|
if in.st.State().Detail == "Release push-to-talk" || in.st.State().Transmitting {
|
|
t.Fatalf("State = %+v, want the notice cleared after release", in.st.State())
|
|
}
|
|
}
|
|
|
|
func TestCertificateProblemStaysOnTheStation(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,\"certificateDir\":\"\",\"enableLogging\":false,\"logFile\":\"/tmp/funknetz.log\"}\n")
|
|
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.ConfigOpen || got.NeedsConfig || got.Detail != "certificateDir is empty" || got.Connection != "Disconnected" {
|
|
t.Fatalf("State = %+v, want the station window with the certificate detail", got)
|
|
}
|
|
if st.Cancel() {
|
|
t.Fatal("Cancel() quit after a certificate-directory problem")
|
|
}
|
|
}
|
|
|
|
func TestOpenConfigDoesNotRewriteAHandEdit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "funknetz.json")
|
|
body := []byte(fullConfig("127.0.0.1", 5308, "DL1ABC", "key", 20, filepath.Join(dir, "funknetz.log")))
|
|
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)
|
|
edited := []byte("{\"host\":\"127.0.0.1\",\"port\":5308,\"callsign\":\"DL1ABC\",\"authKey\":\"key\",\"talkGroup\":20,\"enableLogging\":false,\"logFile\":\"" + filepath.Join(dir, "funknetz.log") + "\"}\n")
|
|
if err := os.WriteFile(path, edited, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st.OpenConfig()
|
|
if st.Cancel() {
|
|
t.Fatal("Cancel() quit")
|
|
}
|
|
got, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != string(edited) {
|
|
t.Fatalf("file = %s, want the hand-edit left unchanged", got)
|
|
}
|
|
if st.State().Callsign != "DL1ABC" || st.State().ConfigOpen {
|
|
t.Fatalf("State = %+v, want the loaded session and a closed Config window", st.State())
|
|
}
|
|
}
|
|
|
|
func TestSaveEmptyCertificateDirDoesNotConnect(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: "127.0.0.1",
|
|
Port: port,
|
|
Callsign: "DL1ABC",
|
|
AuthKey: "key",
|
|
TalkGroup: 20,
|
|
CertificateDir: "",
|
|
LogFile: "/tmp/funknetz.log",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := st.State()
|
|
if got.ConfigOpen || got.Detail != "certificateDir is empty" || got.Connection != "Disconnected" {
|
|
t.Fatalf("State = %+v, want the window closed and the certificate detail", got)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm() != 0o600 {
|
|
t.Fatalf("mode = %o, want 600", info.Mode().Perm())
|
|
}
|
|
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 TestLoggingOnlySaveKeepsTheSession(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
logPath := filepath.Join(t.TempDir(), "station.log")
|
|
in.st.OpenConfig()
|
|
form := in.st.Config().Form
|
|
form.EnableLogging = true
|
|
form.LogFile = logPath
|
|
if err := in.st.Save(form); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if in.st.Config().Open {
|
|
t.Fatal("Config window stayed open")
|
|
}
|
|
state := in.st.State()
|
|
if state.Connection != "Connected" || state.TalkGroup != 20 || state.Callsign != "DL1ABC" {
|
|
t.Fatalf("State = %+v, want the session kept", state)
|
|
}
|
|
if _, err := os.Stat(logPath); err != nil {
|
|
t.Fatalf("log file: %v", err)
|
|
}
|
|
if err := connStillOpen(in.refl.conn()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestLoggingSaveDoesNotClearAFatalStop(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 := writeConfigFile(t, fullConfig("127.0.0.1", port, "DL1ABC", "key", 20, filepath.Join(dir, "funknetz.log")))
|
|
st, err := station.New(station.Options{ConfigPath: path})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(st.Close)
|
|
st.Fail("could not open the audio device")
|
|
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)
|
|
}()
|
|
|
|
st.OpenConfig()
|
|
form := st.Config().Form
|
|
form.EnableLogging = true
|
|
form.LogFile = filepath.Join(dir, "station.log")
|
|
if err := st.Save(form); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if st.State().Detail != "could not open the audio device" || st.State().Connection != "Disconnected" {
|
|
t.Fatalf("State = %+v, want the fatal stop kept", 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 TestUnchangedSaveDoesNotReconnect(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
in.st.OpenConfig()
|
|
form := in.st.Config().Form
|
|
if err := in.st.Save(form); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if in.st.Config().Open || in.st.State().Connection != "Connected" {
|
|
t.Fatalf("State = %+v, Config open = %v, want the window closed and the session kept", in.st.State(), in.st.Config().Open)
|
|
}
|
|
if err := connStillOpen(in.refl.conn()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestSaveWriteFailureKeepsTheSession(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
before, err := os.ReadFile(in.path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dir := filepath.Dir(in.path)
|
|
if err := os.Chmod(dir, 0o555); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
// Restore the directory so the test temp dir can be removed.
|
|
_ = os.Chmod(dir, 0o755)
|
|
})
|
|
in.st.OpenConfig()
|
|
form := in.st.Config().Form
|
|
form.TalkGroup = 21
|
|
err = in.st.Save(form)
|
|
if err == nil || !strings.Contains(err.Error(), "the file could not be written") {
|
|
t.Fatalf("Save() error = %v, want a write failure", err)
|
|
}
|
|
state := in.st.State()
|
|
if !state.ConfigOpen || state.Connection != "Connected" || state.TalkGroup != 20 || !strings.Contains(state.Detail, "the file could not be written") {
|
|
t.Fatalf("State = %+v, want the window open, the session kept, and the write error", state)
|
|
}
|
|
if err := os.Chmod(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := os.ReadFile(in.path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != string(before) {
|
|
t.Fatalf("file = %s, want it unchanged", got)
|
|
}
|
|
if err := connStillOpen(in.refl.conn()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestEachConnectionFieldReconnects(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
edit func(*station.ConfigForm, int)
|
|
}{
|
|
{name: "host", edit: func(f *station.ConfigForm, _ int) { f.Host = "localhost" }},
|
|
{name: "callsign", edit: func(f *station.ConfigForm, _ int) { f.Callsign = "DL1ABD" }},
|
|
{name: "authKey", edit: func(f *station.ConfigForm, _ int) { f.AuthKey = "other-key" }},
|
|
{name: "certificateDir", edit: func(f *station.ConfigForm, _ int) { f.CertificateDir = "./radios" }},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
in.st.OpenConfig()
|
|
form := in.st.Config().Form
|
|
tt.edit(&form, in.refl.port)
|
|
if err := in.st.Save(form); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if in.st.Config().Open {
|
|
t.Fatal("Config window stayed open")
|
|
}
|
|
old := in.refl.conn()
|
|
in.refl.accept(t)
|
|
if err := connStillOpen(old); err == nil {
|
|
t.Fatal("old session stayed open")
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("port", func(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
in := login(t, "DL1ABC", "key", 20, 7, []string{"OPUS"})
|
|
in.st.OpenConfig()
|
|
form := in.st.Config().Form
|
|
form.Port = ln.Addr().(*net.TCPAddr).Port
|
|
if err := in.st.Save(form); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := in.refl.conn()
|
|
ln.(*net.TCPListener).SetDeadline(time.Now().Add(2 * time.Second))
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn.Close()
|
|
if err := connStillOpen(old); err == nil {
|
|
t.Fatal("old session stayed open")
|
|
}
|
|
})
|
|
}
|
|
|
|
func fullConfig(host string, port int, callsign, authKey string, talkGroup int, logFile string) string {
|
|
return "{\"host\":\"" + host + "\",\"port\":" + strconv.Itoa(port) + ",\"callsign\":\"" + callsign + "\",\"authKey\":\"" + authKey + "\",\"talkGroup\":" + strconv.Itoa(talkGroup) + ",\"certificateDir\":\"./funknetz\",\"enableLogging\":false,\"logFile\":\"" + logFile + "\"}\n"
|
|
}
|
|
|
|
func connStillOpen(conn net.Conn) error {
|
|
if err := conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
|
|
return err
|
|
}
|
|
var hdr [4]byte
|
|
_, err := io.ReadFull(conn, hdr[:])
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if ne, ok := errors.AsType[net.Error](err); ok && ne.Timeout() {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|