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>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user