package station_test import ( "crypto/tls" "encoding/binary" "encoding/json" "io" "net" "os" "sync" "testing" "time" "gitea.neitzel.de/konrad/funknetz/station" ) type fakeClock struct { mu sync.Mutex now time.Time waiters []*fakeWaiter gen int } type fakeWaiter struct { deadline time.Time ch chan time.Time done bool } func newFakeClock() *fakeClock { return &fakeClock{now: time.Date(2026, 10, 3, 12, 0, 0, 0, time.UTC)} } func (c *fakeClock) Now() time.Time { c.mu.Lock() defer c.mu.Unlock() return c.now } func (c *fakeClock) NewTimer(d time.Duration) station.Timer { c.mu.Lock() defer c.mu.Unlock() if d < 0 { d = 0 } ch := make(chan time.Time, 1) c.gen++ w := &fakeWaiter{deadline: c.now.Add(d), ch: ch} if d == 0 { ch <- c.now w.done = true return &fakeTimer{c: c, w: w} } c.waiters = append(c.waiters, w) return &fakeTimer{c: c, w: w} } func (c *fakeClock) Advance(d time.Duration) { c.mu.Lock() defer c.mu.Unlock() c.now = c.now.Add(d) kept := c.waiters[:0] for _, w := range c.waiters { if w.done { continue } if !w.deadline.After(c.now) { w.done = true w.ch <- c.now continue } kept = append(kept, w) } c.waiters = kept } func (c *fakeClock) Generation() int { c.mu.Lock() defer c.mu.Unlock() return c.gen } func waitClock(t *testing.T, c *fakeClock, after int) { t.Helper() deadline := time.Now().Add(2 * time.Second) for c.Generation() <= after { if time.Now().After(deadline) { t.Fatal("station did not rearm the clock") } time.Sleep(time.Millisecond) } } func (c *fakeClock) BlockUntilWaiting(t *testing.T) { t.Helper() deadline := time.Now().Add(2 * time.Second) for { c.mu.Lock() n := 0 for _, w := range c.waiters { if !w.done { n++ } } c.mu.Unlock() if n > 0 { return } if time.Now().After(deadline) { t.Fatal("station did not wait on the clock") } time.Sleep(time.Millisecond) } } type fakeTimer struct { c *fakeClock w *fakeWaiter } func (t *fakeTimer) C() <-chan time.Time { return t.w.ch } func (t *fakeTimer) Stop() bool { t.c.mu.Lock() defer t.c.mu.Unlock() if t.w.done { return false } t.w.done = true return true } type reflector struct { ln net.Listener udp *net.UDPConn tcp net.Conn tls *tls.Conn peer *net.UDPAddr port int } func (r *reflector) conn() net.Conn { if r.tls != nil { return r.tls } return r.tcp } func newReflector(t *testing.T) *reflector { t.Helper() var ln net.Listener var udp *net.UDPConn var port int for range 8 { var err error ln, err = net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } port = ln.Addr().(*net.TCPAddr).Port udp, err = net.ListenUDP("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: port}) if err == nil { break } ln.Close() ln = nil } if udp == nil { t.Fatal("udp port stayed in use") } t.Cleanup(func() { if ln != nil { ln.Close() } udp.Close() }) return &reflector{ln: ln, udp: udp, port: port} } func (r *reflector) accept(t *testing.T) { t.Helper() r.ln.(*net.TCPListener).SetDeadline(time.Now().Add(2 * time.Second)) conn, err := r.ln.Accept() if err != nil { t.Fatal(err) } r.tcp = conn r.tls = nil t.Cleanup(func() { conn.Close() }) } func (r *reflector) readFrame(t *testing.T) (uint16, []byte) { t.Helper() conn := r.conn() if err := conn.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { t.Fatal(err) } var hdr [4]byte if _, err := io.ReadFull(conn, hdr[:]); err != nil { t.Fatal(err) } n := binary.BigEndian.Uint32(hdr[:]) if n > 32768 { t.Fatalf("frame length %d", n) } buf := make([]byte, n) if _, err := io.ReadFull(conn, buf); err != nil { t.Fatal(err) } if len(buf) < 2 { t.Fatalf("short frame %d", len(buf)) } return binary.BigEndian.Uint16(buf[:2]), buf[2:] } func (r *reflector) writeFrame(t *testing.T, typ uint16, body []byte) { t.Helper() payload := make([]byte, 2+len(body)) binary.BigEndian.PutUint16(payload, typ) copy(payload[2:], body) var hdr [4]byte binary.BigEndian.PutUint32(hdr[:], uint32(len(payload))) conn := r.conn() if err := conn.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil { t.Fatal(err) } if _, err := conn.Write(append(hdr[:], payload...)); err != nil { t.Fatal(err) } } func (r *reflector) handshake(t *testing.T, cfg *tls.Config) error { t.Helper() r.tls = tls.Server(r.tcp, cfg) if err := r.tls.SetDeadline(time.Now().Add(2 * time.Second)); err != nil { return err } err := r.tls.Handshake() _ = r.tls.SetDeadline(time.Time{}) if err != nil { r.tls = nil } return err } func (r *reflector) readUDP(t *testing.T) (typ, id, seq uint16, body []byte) { t.Helper() if err := r.udp.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { t.Fatal(err) } buf := make([]byte, 2048) n, addr, err := r.udp.ReadFromUDP(buf) if err != nil { t.Fatal(err) } r.peer = addr if n < 6 { t.Fatalf("short datagram %d", n) } typ = binary.BigEndian.Uint16(buf[0:2]) id = binary.BigEndian.Uint16(buf[2:4]) seq = binary.BigEndian.Uint16(buf[4:6]) body = append([]byte(nil), buf[6:n]...) return typ, id, seq, body } func (r *reflector) readRawUDP(t *testing.T) []byte { t.Helper() if err := r.udp.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil { t.Fatal(err) } buf := make([]byte, 2048) n, addr, err := r.udp.ReadFromUDP(buf) if err != nil { t.Fatal(err) } r.peer = addr return append([]byte(nil), buf[:n]...) } func (r *reflector) writeRawUDP(t *testing.T, pkt []byte) { t.Helper() if r.peer == nil { t.Fatal("no udp peer") } if err := r.udp.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil { t.Fatal(err) } if _, err := r.udp.WriteToUDP(pkt, r.peer); err != nil { t.Fatal(err) } } func (r *reflector) writeUDP(t *testing.T, typ, id, seq uint16, body []byte) { t.Helper() if r.peer == nil { t.Fatal("no udp peer") } buf := make([]byte, 6+len(body)) binary.BigEndian.PutUint16(buf[0:2], typ) binary.BigEndian.PutUint16(buf[2:4], id) binary.BigEndian.PutUint16(buf[4:6], seq) copy(buf[6:], body) if err := r.udp.SetWriteDeadline(time.Now().Add(2 * time.Second)); err != nil { t.Fatal(err) } if _, err := r.udp.WriteToUDP(buf, r.peer); err != nil { t.Fatal(err) } } func appendU16(b []byte, v uint16) []byte { var buf [2]byte binary.BigEndian.PutUint16(buf[:], v) return append(b, buf[:]...) } func appendU32(b []byte, v uint32) []byte { var buf [4]byte binary.BigEndian.PutUint32(buf[:], v) return append(b, buf[:]...) } func appendStr(b []byte, s string) []byte { b = appendU16(b, uint16(len(s))) return append(b, s...) } func appendBytes(b []byte, p []byte) []byte { b = appendU16(b, uint16(len(p))) return append(b, p...) } func takeU16(t *testing.T, b []byte) (uint16, []byte) { t.Helper() if len(b) < 2 { t.Fatalf("need 2 bytes, have %d", len(b)) } return binary.BigEndian.Uint16(b[:2]), b[2:] } func takeU32(t *testing.T, b []byte) (uint32, []byte) { t.Helper() if len(b) < 4 { t.Fatalf("need 4 bytes, have %d", len(b)) } return binary.BigEndian.Uint32(b[:4]), b[4:] } func requireSoftwareIdentity(t *testing.T, body []byte) { t.Helper() raw, rest := takeStr(t, body) if len(rest) != 0 { t.Fatalf("software identity has %d trailing bytes", len(rest)) } var got map[string]string if err := json.Unmarshal([]byte(raw), &got); err != nil { t.Fatalf("software identity %q: %v", raw, err) } if len(got) != 3 || got["sw"] != station.SoftwareName || got["swVer"] != station.SoftwareVersion || got["projVer"] != station.SoftwareVersion { t.Fatalf("software identity = %#v, want sw, swVer, and projVer %q", got, station.SoftwareName) } if _, ok := got["machineArch"]; ok { t.Fatal("protocol 2.0 node information includes machineArch") } } func takeStr(t *testing.T, b []byte) (string, []byte) { t.Helper() n, rest := takeU16(t, b) if len(rest) < int(n) { t.Fatalf("string needs %d bytes, have %d", n, len(rest)) } return string(rest[:n]), rest[n:] } func takeBytes(t *testing.T, b []byte) ([]byte, []byte) { t.Helper() n, rest := takeU16(t, b) if len(rest) < int(n) { t.Fatalf("bytes need %d, have %d", n, len(rest)) } return append([]byte(nil), rest[:n]...), rest[n:] } func serverInfo(clientID uint16, nodes, codecs []string) []byte { b := appendU16(nil, 0) b = appendU16(b, clientID) b = appendU16(b, uint16(len(nodes))) for _, n := range nodes { b = appendStr(b, n) } b = appendU16(b, uint16(len(codecs))) for _, c := range codecs { b = appendStr(b, c) } return b } func writeConfigFile(t *testing.T, body string) string { t.Helper() path := t.TempDir() + "/funknetz.json" if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } return path }