Switch the lab reflector to jayReflector and harden local station use.
Build DJ1JAY’s fork with an optional Satellite/Trunk stub, allow -c/--config, open capture and playback as separate devices, and add a lab start helper. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,9 +31,9 @@ func login(t *testing.T, callsign, key string, tg int, id uint16, codecs []strin
|
||||
func loginLog(t *testing.T, callsign, key string, tg int, id uint16, codecs []string, logPath string) loggedIn {
|
||||
t.Helper()
|
||||
refl := newReflector(t)
|
||||
file := configJSON("127.0.0.1", refl.port, callsign, key, tg)
|
||||
file := configJSON(t, "127.0.0.1", refl.port, callsign, key, tg)
|
||||
if logPath != "" {
|
||||
file = configWithLog("127.0.0.1", refl.port, callsign, key, tg, logPath)
|
||||
file = configWithLog(t, "127.0.0.1", refl.port, callsign, key, tg, logPath)
|
||||
}
|
||||
path := writeConfigFile(t, file)
|
||||
clock := newFakeClock()
|
||||
@@ -151,7 +151,7 @@ func TestRefusedConnectionRetriesUntilQuit(t *testing.T) {
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
ln.Close()
|
||||
|
||||
path := writeConfigFile(t, configJSON("127.0.0.1", port, "DL1ABC", "key", 4))
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", port, "DL1ABC", "key", 4))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
@@ -196,7 +196,7 @@ func TestReflectorErrorStopsRetries(t *testing.T) {
|
||||
for _, msg := range []string{"Access denied", "no such station"} {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON("127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
@@ -230,7 +230,7 @@ func TestReflectorErrorStopsRetries(t *testing.T) {
|
||||
|
||||
func TestMissingOpusStopsRetries(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON("127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
path := writeConfigFile(t, configJSON(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
|
||||
+6
-4
@@ -1,7 +1,9 @@
|
||||
package station
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -48,13 +50,13 @@ var callsignPattern = regexp.MustCompile(`^[A-Z0-9][A-Z]{0,2}\d[A-Z0-9]{0,3}[A-Z
|
||||
func loadConfig(path string) (loaded, error) {
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if err := writeTemplate(path); err != nil {
|
||||
return loaded{}, err
|
||||
}
|
||||
return loaded{}, fmt.Errorf("created %s", path)
|
||||
}
|
||||
return loaded{}, fmt.Errorf("could not be read")
|
||||
return loaded{}, fmt.Errorf("the file could not be read: %w", err)
|
||||
}
|
||||
return parseConfig(path, body)
|
||||
}
|
||||
@@ -83,7 +85,7 @@ func writeTemplate(path string) error {
|
||||
func parseConfig(path string, body []byte) (loaded, error) {
|
||||
var shape fileShape
|
||||
if err := jsonv2.Unmarshal(body, &shape); err != nil {
|
||||
return loaded{}, fmt.Errorf("could not be read")
|
||||
return loaded{}, fmt.Errorf("the file could not be read: %w", err)
|
||||
}
|
||||
if block, notice := blockedLog(shape); block {
|
||||
cfg, err := parseOperational(shape)
|
||||
@@ -99,7 +101,7 @@ func parseConfig(path string, body []byte) (loaded, error) {
|
||||
}
|
||||
shape = withLogDefaults(shape)
|
||||
if err := rewriteConfig(path, shape); err != nil {
|
||||
return loaded{}, fmt.Errorf("could not be read")
|
||||
return loaded{}, fmt.Errorf("the file could not be read: %w", err)
|
||||
}
|
||||
}
|
||||
cfg, err := parseOperational(shape)
|
||||
|
||||
+33
-22
@@ -84,32 +84,41 @@ func TestExistingConfigIsNotRewritten(t *testing.T) {
|
||||
|
||||
func TestInvalidConfigNamesTheField(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
want string
|
||||
name string
|
||||
host string
|
||||
port int
|
||||
callsign string
|
||||
authKey string
|
||||
talkGroup int
|
||||
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: "empty host", host: "", port: 5308, callsign: "DL1ABC", authKey: "key", talkGroup: 1, want: "host is empty"},
|
||||
{name: "blank host", host: " ", port: 5308, callsign: "DL1ABC", authKey: "key", talkGroup: 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: "empty callsign", host: "127.0.0.1", port: 5308, callsign: "", authKey: "key", talkGroup: 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: "empty auth key", host: "127.0.0.1", port: 5308, callsign: "DL1ABC", authKey: "", talkGroup: 1, want: "authKey is empty"},
|
||||
{name: "whitespace auth key", host: "127.0.0.1", port: 5308, callsign: "DL1ABC", authKey: " \t", talkGroup: 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 zero", host: "127.0.0.1", port: 0, callsign: "DL1ABC", authKey: "key", talkGroup: 1, want: "port is invalid"},
|
||||
{name: "port negative", host: "127.0.0.1", port: -1, callsign: "DL1ABC", authKey: "key", talkGroup: 1, want: "port is invalid"},
|
||||
{name: "port too high", host: "127.0.0.1", port: 65536, callsign: "DL1ABC", authKey: "key", talkGroup: 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 zero", host: "127.0.0.1", port: 5308, callsign: "DL1ABC", authKey: "key", talkGroup: 0, want: "talkGroup is invalid"},
|
||||
{name: "talk group negative", host: "127.0.0.1", port: 5308, callsign: "DL1ABC", authKey: "key", talkGroup: -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"},
|
||||
{name: "bad callsign", host: "127.0.0.1", port: 5308, callsign: "HELLO", authKey: "key", talkGroup: 1, want: "callsign is invalid"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := tt.body
|
||||
if body == "" {
|
||||
body = configJSON(t, tt.host, tt.port, tt.callsign, tt.authKey, tt.talkGroup)
|
||||
}
|
||||
path := filepath.Join(t.TempDir(), "funknetz.json")
|
||||
if err := os.WriteFile(path, []byte(tt.body), 0o600); err != nil {
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, err := os.ReadFile(path)
|
||||
@@ -156,8 +165,8 @@ func TestBrokenConfigSaysTheFileCouldNotBeRead(t *testing.T) {
|
||||
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(), "the file could not be read") {
|
||||
t.Fatalf("New() error = %q, want it to say the file could not be read", err.Error())
|
||||
}
|
||||
if strings.Contains(err.Error(), "callsign") {
|
||||
t.Fatalf("New() error = %q, want no field name", err.Error())
|
||||
@@ -166,7 +175,7 @@ func TestBrokenConfigSaysTheFileCouldNotBeRead(t *testing.T) {
|
||||
|
||||
func TestCallsignIsTrimmedAndUppercased(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "funknetz.json")
|
||||
body := configJSON(" 127.0.0.1 ", 5308, " dl1abc-r ", "a b", 20)
|
||||
body := configJSON(t, " 127.0.0.1 ", 5308, " dl1abc-r ", "a b", 20)
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -359,7 +368,8 @@ func TestLogSettingsDoNotStopTheStation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func configJSON(host string, port int, callsign, authKey string, talkGroup int) string {
|
||||
func configJSON(t testing.TB, host string, port int, callsign, authKey string, talkGroup int) string {
|
||||
t.Helper()
|
||||
body, err := jsonv2.Marshal(map[string]any{
|
||||
"host": host,
|
||||
"port": port,
|
||||
@@ -368,12 +378,13 @@ func configJSON(host string, port int, callsign, authKey string, talkGroup int)
|
||||
"talkGroup": talkGroup,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func configWithLog(host string, port int, callsign, authKey string, talkGroup int, logPath string) string {
|
||||
func configWithLog(t testing.TB, host string, port int, callsign, authKey string, talkGroup int, logPath string) string {
|
||||
t.Helper()
|
||||
body, err := jsonv2.Marshal(map[string]any{
|
||||
"host": host,
|
||||
"port": port,
|
||||
@@ -384,7 +395,7 @@ func configWithLog(host string, port int, callsign, authKey string, talkGroup in
|
||||
"logFile": logPath,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestConnectsToLabReflector(t *testing.T) {
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
path := writeConfigFile(t, configJSON(labHost, labPort, labCallsign, labAuthKey, labTalkGroup))
|
||||
path := writeConfigFile(t, configJSON(t, labHost, labPort, labCallsign, labAuthKey, labTalkGroup))
|
||||
st, err := station.New(station.Options{ConfigPath: path})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
+2
-2
@@ -123,7 +123,7 @@ func TestLogFailureStaysInTheIdleSlot(t *testing.T) {
|
||||
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))
|
||||
path := writeConfigFile(t, configWithLog(t, "127.0.0.1", ln, "DL1ABC", "key", 4, logPath))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
@@ -152,7 +152,7 @@ func TestFirstDialFailureIsLoggedOnce(t *testing.T) {
|
||||
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))
|
||||
path := writeConfigFile(t, configWithLog(t, "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 {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
func TestLoginAnnouncesProtocolAndJoinsTheTalkGroup(t *testing.T) {
|
||||
refl := newReflector(t)
|
||||
path := writeConfigFile(t, configJSON(" 127.0.0.1 ", refl.port, " dl1abc ", "auth key with spaces", 20))
|
||||
path := writeConfigFile(t, configJSON(t, " 127.0.0.1 ", refl.port, " dl1abc ", "auth key with spaces", 20))
|
||||
clock := newFakeClock()
|
||||
st, err := station.New(station.Options{ConfigPath: path, Clock: clock})
|
||||
if err != nil {
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package station
|
||||
|
||||
import "encoding/json"
|
||||
import jsonv2 "encoding/json/v2"
|
||||
|
||||
// SoftwareName and SoftwareVersion are the identity sent to the reflector
|
||||
// after login. Edit them here.
|
||||
@@ -10,7 +10,7 @@ const (
|
||||
)
|
||||
|
||||
func softwareIdentityBody() ([]byte, error) {
|
||||
raw, err := json.Marshal(struct {
|
||||
raw, err := jsonv2.Marshal(struct {
|
||||
SW string `json:"sw"`
|
||||
SWVer string `json:"swVer"`
|
||||
}{
|
||||
|
||||
Reference in New Issue
Block a user