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>
198 lines
5.8 KiB
Go
198 lines
5.8 KiB
Go
package station_test
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.neitzel.de/konrad/funknetz/station"
|
|
)
|
|
|
|
func TestLogAppendsSessionEvents(t *testing.T) {
|
|
logPath := filepath.Join(t.TempDir(), "funknetz.log")
|
|
if err := os.WriteFile(logPath, []byte("kept\n"), 0o640); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in := loginLog(t, "DL1ABC", "super-secret-key", 20, 7, []string{"OPUS"}, logPath)
|
|
|
|
in.clock.Advance(2 * time.Second)
|
|
in.refl.writeFrame(t, 104, appendStr(appendU32(nil, 20), "DB0XYZ"))
|
|
if _, err := in.st.Wait(context.Background(), func(s station.State) bool { return s.Talker == "DB0XYZ" }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.clock.Advance(3 * time.Second)
|
|
in.refl.tcp.Close()
|
|
if _, err := in.st.Wait(context.Background(), func(s station.State) bool { return s.Connection == "Disconnected" }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got := readLog(t, logPath)
|
|
want := strings.Join([]string{
|
|
"kept",
|
|
"2026-10-03T12:00:00Z connected",
|
|
"2026-10-03T12:00:02Z DB0XYZ talked for 3 seconds.",
|
|
"2026-10-03T12:00:05Z disconnected",
|
|
"",
|
|
}, "\n")
|
|
if got != want {
|
|
t.Fatalf("log = %q, want %q", got, want)
|
|
}
|
|
if strings.Contains(got, "super-secret-key") {
|
|
t.Fatal("log contains the auth key")
|
|
}
|
|
info, err := os.Stat(logPath)
|
|
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 TestLogRecordsSendingAndARemoteTalker(t *testing.T) {
|
|
logPath := filepath.Join(t.TempDir(), "funknetz.log")
|
|
in := loginLog(t, "DL1ABC", "key", 20, 7, []string{"OPUS"}, logPath)
|
|
|
|
in.st.HoldButton()
|
|
in.st.ReleaseButton()
|
|
in.clock.Advance(2 * time.Second)
|
|
in.st.HoldButton()
|
|
in.refl.writeFrame(t, 104, appendStr(appendU32(nil, 20), "DL1ABC"))
|
|
in.clock.Advance(2 * time.Second)
|
|
in.refl.writeFrame(t, 104, appendStr(appendU32(nil, 20), "DB0XYZ"))
|
|
if _, err := in.st.Wait(context.Background(), func(s station.State) bool { return s.Talker == "DB0XYZ" }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
in.st.HoldButton()
|
|
in.st.ReleaseButton()
|
|
in.clock.Advance(4 * time.Second)
|
|
in.refl.writeFrame(t, 105, appendStr(appendU32(nil, 20), "DB0XYZ"))
|
|
if _, err := in.st.Wait(context.Background(), func(s station.State) bool { return s.Talker == "" && !s.Transmitting }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got := readLog(t, logPath)
|
|
want := strings.Join([]string{
|
|
"2026-10-03T12:00:00Z connected",
|
|
"2026-10-03T12:00:00Z sending for 0 seconds.",
|
|
"2026-10-03T12:00:02Z sending for 2 seconds.",
|
|
"2026-10-03T12:00:04Z DB0XYZ talked for 4 seconds.",
|
|
"",
|
|
}, "\n")
|
|
if got != want {
|
|
t.Fatalf("log = %q, want %q", got, want)
|
|
}
|
|
info, err := os.Stat(logPath)
|
|
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)
|
|
}
|
|
}
|
|
|
|
func TestLogFailureStaysInTheIdleSlot(t *testing.T) {
|
|
logPath := filepath.Join(t.TempDir(), "missing", "funknetz.log")
|
|
in := loginLog(t, "DL1ABC", "key", 20, 7, []string{"OPUS"}, logPath)
|
|
notice := "Cannot write " + logPath
|
|
if got := in.st.State(); got.Connection != "Connected" || got.Detail != notice {
|
|
t.Fatalf("State = %+v, want Connected and the log failure", got)
|
|
}
|
|
in.refl.writeFrame(t, 104, appendStr(appendU32(nil, 20), "DB0XYZ"))
|
|
got, err := in.st.Wait(context.Background(), func(s station.State) bool { return s.Talker == "DB0XYZ" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Detail != "Idle" {
|
|
t.Fatalf("Detail = %q, want the talker to take the line", got.Detail)
|
|
}
|
|
in.refl.writeFrame(t, 105, appendStr(appendU32(nil, 20), "DB0XYZ"))
|
|
got, err = in.st.Wait(context.Background(), func(s station.State) bool { return s.Talker == "" })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Detail != notice {
|
|
t.Fatalf("Detail = %q, want the log failure back", got.Detail)
|
|
}
|
|
}
|
|
|
|
func TestFirstDialFailureIsLoggedOnce(t *testing.T) {
|
|
logPath := filepath.Join(t.TempDir(), "funknetz.log")
|
|
ln := listenClosed(t)
|
|
path := writeConfigFile(t, configWithLog("127.0.0.1", ln, "DL1ABC", "key", 4, logPath))
|
|
clock := newFakeClock()
|
|
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
go func() { _ = st.Run(ctx) }()
|
|
|
|
want := "2026-10-03T12:00:00Z disconnected\n"
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for readLog(t, logPath) != want {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("log = %q, want %q", readLog(t, logPath), want)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
clock.BlockUntilWaiting(t)
|
|
clock.Advance(2 * time.Second)
|
|
clock.BlockUntilWaiting(t)
|
|
if got := readLog(t, logPath); got != want {
|
|
t.Fatalf("log = %q, want the dial failure logged once", got)
|
|
}
|
|
}
|
|
|
|
func TestReflectorErrorsAreOneLogLine(t *testing.T) {
|
|
logPath := filepath.Join(t.TempDir(), "funknetz.log")
|
|
refl := newReflector(t)
|
|
path := writeConfigFile(t, configWithLog("127.0.0.1", refl.port, "DL1ABC", "key", 20, logPath))
|
|
clock := newFakeClock()
|
|
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
go func() { _ = st.Run(ctx) }()
|
|
refl.accept(t)
|
|
refl.readFrame(t)
|
|
refl.writeFrame(t, 13, appendStr(nil, "bad\nline"))
|
|
if _, err := st.Wait(ctx, func(s station.State) bool { return s.Detail == "bad\nline" }); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := readLog(t, logPath)
|
|
if got != "2026-10-03T12:00:00Z bad line\n" {
|
|
t.Fatalf("log = %q", got)
|
|
}
|
|
if strings.Contains(got, "disconnected") {
|
|
t.Fatalf("log = %q, want no disconnected line", got)
|
|
}
|
|
}
|
|
|
|
func listenClosed(t *testing.T) int {
|
|
t.Helper()
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
ln.Close()
|
|
return port
|
|
}
|
|
|
|
func readLog(t *testing.T, path string) string {
|
|
t.Helper()
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return string(b)
|
|
}
|