Files
funknetz/station/protocol_select_test.go
konradandCursor 2c621d4b7f Let the config choose protocol 2 or 3, and fall back for one session only.
The default stays v2. A reflector that refuses that version and names the other gets one session of it, then the station announces the configured protocol again.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-22 16:28:42 +02:00

215 lines
6.3 KiB
Go

package station_test
import (
"context"
"net"
"os"
"testing"
"time"
"gitea.neitzel.de/konrad/funknetz/station"
)
func TestVersionErrorFallsBackWhenTheMaximumIs2(t *testing.T) {
for _, msg := range []string{
"Unsupported protocol version 3.0. May be at most 2.0.",
"Unsupported protocol version 3.0. May be at most 2.5.",
} {
t.Run(msg, func(t *testing.T) {
refl := newReflector(t)
path := writeConfigFile(t, configJSONProtocol(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, "v3"))
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() {
// Run's error is observed through State, which is the station seam.
_ = st.Run(ctx)
}()
refl.accept(t)
typ, body := refl.readFrame(t)
if typ != 5 || !versionIs(body, 3, 0) {
t.Fatalf("type %d body %v", typ, body)
}
refl.writeFrame(t, 13, appendStr(nil, msg))
refl.accept(t)
typ, body = refl.readFrame(t)
if typ != 5 || !versionIs(body, 2, 0) {
t.Fatalf("fallback type %d body %v", typ, body)
}
})
}
}
func TestVersionErrorBelow2Stops(t *testing.T) {
for _, msg := range []string{
"Unsupported protocol version 3.0. May be at most 1.9.",
"Unsupported protocol version 3.0. May be at most 0.6.",
} {
t.Run(msg, func(t *testing.T) {
refl := newReflector(t)
path := writeConfigFile(t, configJSONProtocol(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, "v3"))
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() {
// Run's error is observed through State, which is the station seam.
_ = st.Run(ctx)
}()
refl.accept(t)
refl.readFrame(t)
refl.writeFrame(t, 13, appendStr(nil, msg))
got, err := st.Wait(ctx, func(s station.State) bool { return s.Detail == msg })
if err != nil {
t.Fatal(err)
}
if got.Connection != "Disconnected" || got.Protocol != "" {
t.Fatalf("State = %+v", got)
}
clock.Advance(2 * time.Second)
refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
if conn, err := refl.ln.Accept(); err == nil {
conn.Close()
t.Fatal("retried")
}
})
}
}
func TestDowngradeOtherThan2Stops(t *testing.T) {
for _, tc := range []struct {
major, minor uint16
want string
}{
{1, 0, "Unsupported protocol version 1.0"},
{4, 2, "Unsupported protocol version 4.2"},
} {
t.Run(tc.want, func(t *testing.T) {
refl := newReflector(t)
path := writeConfigFile(t, configJSONProtocol(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, "v3"))
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() {
// Run's error is observed through State, which is the station seam.
_ = st.Run(ctx)
}()
refl.accept(t)
refl.readFrame(t)
refl.writeFrame(t, 6, versionWire(tc.major, tc.minor))
got, err := st.Wait(ctx, func(s station.State) bool { return s.Detail == tc.want })
if err != nil {
t.Fatal(err)
}
if got.Protocol != "" || got.Connection != "Disconnected" {
t.Fatalf("State = %+v", got)
}
clock.Advance(2 * time.Second)
refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
if conn, err := refl.ln.Accept(); err == nil {
conn.Close()
t.Fatal("retried")
}
})
}
}
func TestDowngradeDoesNotCreateTheCertificateDirectory(t *testing.T) {
refl := newReflector(t)
dir := t.TempDir()
path := dir + "/funknetz.json"
body := configWithDir(t, "127.0.0.1", refl.port, "DL1ABC", "key", 20, "./funknetz", "")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
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() {
// Run's error is observed through State, which is the station seam.
_ = st.Run(ctx)
}()
refl.accept(t)
declineTo2(t, refl)
if _, err := os.Stat(dir + "/funknetz"); err == nil {
t.Fatal("created the certificate directory on a 2.0 fallback")
}
}
func TestConfiguredV2FallsBackTo3ThenReturns(t *testing.T) {
refl := newReflector(t)
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 {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
go func() {
// Run's error is observed through State, which is the station seam.
_ = st.Run(ctx)
}()
refl.accept(t)
expectVersion(t, refl, 2, 0)
refl.writeFrame(t, 13, appendStr(nil, "Unsupported protocol version 2.0. Must be at least 3.0."))
refl.accept(t)
expectVersion(t, refl, 3, 0)
refl.tcp.Close()
if _, err := st.Wait(ctx, func(s station.State) bool { return s.Connection == "Disconnected" }); err != nil {
t.Fatal(err)
}
clock.BlockUntilWaiting(t)
clock.Advance(2 * time.Second)
refl.accept(t)
expectVersion(t, refl, 2, 0)
}
func TestV2DoesNotFallBackAbove3(t *testing.T) {
refl := newReflector(t)
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 {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
go func() {
// Run's error is observed through State, which is the station seam.
_ = st.Run(ctx)
}()
refl.accept(t)
expectVersion(t, refl, 2, 0)
msg := "Unsupported protocol version 2.0. Must be at least 3.1."
refl.writeFrame(t, 13, appendStr(nil, msg))
got, err := st.Wait(ctx, func(s station.State) bool { return s.Detail == msg })
if err != nil {
t.Fatal(err)
}
if got.Connection != "Disconnected" || got.Protocol != "" {
t.Fatalf("State = %+v", got)
}
clock.Advance(2 * time.Second)
refl.ln.(*net.TCPListener).SetDeadline(time.Now().Add(150 * time.Millisecond))
if conn, err := refl.ln.Accept(); err == nil {
conn.Close()
t.Fatal("retried")
}
}