Files
funknetz/station/session_test.go
T
Konrad NeitzelandCursor 9c4f6621fc Join one FM-Funknetz talk group from a headset window.
The station reads ~/.funknetz.json, speaks protocol 2.0, and builds an unsigned funknetz.app.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 16:21:41 +02:00

100 lines
2.9 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(" 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" {
t.Fatalf("State = %+v, want DL1ABC talk group 20 Idle", got)
}
refl.accept(t)
typ, body := refl.readFrame(t)
if typ != 5 || len(body) != 4 || body[0] != 0 || body[1] != 2 || body[2] != 0 || body[3] != 0 {
t.Fatalf("first 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 != 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 != "" {
t.Fatalf("State = %+v, want Connected Idle", got)
}
// No other talk group and no station profile: the next TCP message is a heartbeat, not type 107 or 111.
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)
}
}