Append an optional station log and announce the software after login.
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>
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user