Files
funknetz/station/session_test.go
T
konradandCursor 3d1a2a1669 Speak protocol 3.0 and fall back to 2.0 only when that is the reflector's maximum.
A reflector that has moved to 3.0 will not accept a station that always announces 2.0, and a 2.x reflector still needs one explicit plaintext session.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 14:24:00 +02:00

112 lines
3.2 KiB
Go

package station_test
import (
"bytes"
"context"
"crypto/hmac"
"crypto/sha1"
"testing"
"time"
"gitea.neitzel.de/konrad/funknetz/station"
)
func TestLoginAnnouncesProtocolAndJoinsTheTalkGroup(t *testing.T) {
refl := newReflector(t)
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 {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
go func() { _ = st.Run(ctx) }()
got, err := st.Wait(ctx, func(s station.State) bool { return s.Connection == "Connecting" })
if err != nil {
t.Fatal(err)
}
if got.Callsign != "DL1ABC" || got.TalkGroup != 20 || got.Detail != "Idle" || got.Protocol != "" {
t.Fatalf("State = %+v, want DL1ABC talk group 20 Idle and a blank protocol", got)
}
refl.accept(t)
typ, body := refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("first frame type %d body %v, want protocol 3.0", typ, body)
}
refl.writeFrame(t, 6, versionWire(2, 0))
refl.accept(t)
typ, body = refl.readFrame(t)
if typ != 5 || !versionIs(body, 2, 0) {
t.Fatalf("fallback frame type %d body %v, want protocol 2.0", typ, body)
}
challenge := make([]byte, 20)
for i := range challenge {
challenge[i] = byte(i + 1)
}
refl.writeFrame(t, 10, appendBytes(nil, challenge))
typ, body = refl.readFrame(t)
if typ != 11 {
t.Fatalf("auth response type %d, want 11", typ)
}
call, rest := takeStr(t, body)
digest, rest := takeBytes(t, rest)
if len(rest) != 0 {
t.Fatalf("auth response has %d trailing bytes", len(rest))
}
if call != "DL1ABC" {
t.Fatalf("callsign = %q, want DL1ABC", call)
}
mac := hmac.New(sha1.New, []byte("auth key with spaces"))
mac.Write(challenge)
if !hmac.Equal(digest, mac.Sum(nil)) {
t.Fatalf("digest = %x, want hmac-sha1 of the challenge under the stored key", digest)
}
if bytes.Contains(body, []byte("auth key with spaces")) {
t.Fatal("auth response contains the auth key")
}
refl.writeFrame(t, 12, nil)
refl.writeFrame(t, 100, serverInfo(0x1234, []string{"db0abc"}, []string{"GSM", "OPUS"}))
typ, body = refl.readFrame(t)
if typ != 111 {
t.Fatalf("frame type %d, want software identity", typ)
}
requireSoftwareIdentity(t, body)
typ, body = refl.readFrame(t)
if typ != 106 {
t.Fatalf("frame type %d, want select talk group", typ)
}
tg, rest := takeU32(t, body)
if tg != 20 || len(rest) != 0 {
t.Fatalf("select talk group = %d rest %d, want 20", tg, len(rest))
}
utyp, id, seq, ubody := refl.readUDP(t)
if utyp != 1 || id != 0x1234 || seq != 0 || len(ubody) != 0 {
t.Fatalf("udp heartbeat type %d id %d seq %d len %d, want type 1 id 0x1234 seq 0", utyp, id, seq, len(ubody))
}
got, err = st.Wait(ctx, func(s station.State) bool { return s.Connection == "Connected" })
if err != nil {
t.Fatal(err)
}
if got.Detail != "Idle" || got.Talker != "" || got.Protocol != "2.0" {
t.Fatalf("State = %+v, want Connected Idle protocol 2.0", got)
}
// No other talk group: the next TCP message is a heartbeat, not a monitor set.
clock.BlockUntilWaiting(t)
clock.Advance(10 * time.Second)
typ, _ = refl.readFrame(t)
if typ != 1 {
t.Fatalf("quiet TCP frame type %d, want heartbeat 1", typ)
}
}