package transport import ( "testing" "time" pb "github.com/antitbone/dwarfctl/proto" "google.golang.org/protobuf/proto" ) // TestEnvelopeRoundTrip verifies that a WsPacket envelope survives a // marshal → unmarshal round-trip with all fields intact. This is the core // guarantee for every command sent to the telescope. func TestEnvelopeRoundTrip(t *testing.T) { original := &pb.WsPacket{ MajorVersion: WsMajorVersion, MinorVersion: WsMinorVersion, DeviceId: 1, ModuleId: 6, // MODULE_MOTOR Cmd: 14006, Type: uint32(MsgRequest), Data: []byte{0x08, 0x2a}, // some inner proto bytes ClientId: "test-client-007", } raw, err := proto.Marshal(original) if err != nil { t.Fatalf("marshal WsPacket: %v", err) } if len(raw) == 0 { t.Fatal("marshaled envelope is empty") } decoded := &pb.WsPacket{} if err := proto.Unmarshal(raw, decoded); err != nil { t.Fatalf("unmarshal WsPacket: %v", err) } if decoded.GetMajorVersion() != original.GetMajorVersion() { t.Errorf("major_version: got %d, want %d", decoded.GetMajorVersion(), original.GetMajorVersion()) } if decoded.GetMinorVersion() != original.GetMinorVersion() { t.Errorf("minor_version: got %d, want %d", decoded.GetMinorVersion(), original.GetMinorVersion()) } if decoded.GetDeviceId() != original.GetDeviceId() { t.Errorf("device_id: got %d, want %d", decoded.GetDeviceId(), original.GetDeviceId()) } if decoded.GetModuleId() != original.GetModuleId() { t.Errorf("module_id: got %d, want %d", decoded.GetModuleId(), original.GetModuleId()) } if decoded.GetCmd() != original.GetCmd() { t.Errorf("cmd: got %d, want %d", decoded.GetCmd(), original.GetCmd()) } if decoded.GetType() != original.GetType() { t.Errorf("type: got %d, want %d", decoded.GetType(), original.GetType()) } if decoded.GetClientId() != original.GetClientId() { t.Errorf("client_id: got %q, want %q", decoded.GetClientId(), original.GetClientId()) } if string(decoded.GetData()) != string(original.GetData()) { t.Errorf("data: got %x, want %x", decoded.GetData(), original.GetData()) } } // TestEnvelopeWithInnerProto verifies that inner proto messages (the Data // field) encode and decode correctly within the envelope. func TestEnvelopeWithInnerProto(t *testing.T) { inner := &pb.ReqMotorServiceJoystick{ VectorAngle: 90.5, VectorLength: 0.75, } innerBytes, err := proto.Marshal(inner) if err != nil { t.Fatalf("marshal inner: %v", err) } envelope := &pb.WsPacket{ MajorVersion: WsMajorVersion, MinorVersion: WsMinorVersion, DeviceId: 1, ModuleId: 6, Cmd: 14006, Type: uint32(MsgRequest), Data: innerBytes, ClientId: "dwarfctl-test", } raw, err := proto.Marshal(envelope) if err != nil { t.Fatalf("marshal envelope: %v", err) } decoded := &pb.WsPacket{} if err := proto.Unmarshal(raw, decoded); err != nil { t.Fatalf("unmarshal envelope: %v", err) } // Decode the inner message innerDecoded := &pb.ReqMotorServiceJoystick{} if err := proto.Unmarshal(decoded.GetData(), innerDecoded); err != nil { t.Fatalf("unmarshal inner: %v", err) } if innerDecoded.GetVectorAngle() != 90.5 { t.Errorf("vector_angle: got %f, want 90.5", innerDecoded.GetVectorAngle()) } if innerDecoded.GetVectorLength() != 0.75 { t.Errorf("vector_length: got %f, want 0.75", innerDecoded.GetVectorLength()) } } // TestNewClientDefaults verifies that NewClient stores the client_id and // device_id and initializes the pending map. func TestNewClientDefaults(t *testing.T) { c := NewClient("my-client", 2) if c.clientID != "my-client" { t.Errorf("clientID: got %q, want %q", c.clientID, "my-client") } if c.deviceID != 2 { t.Errorf("deviceID: got %d, want 2", c.deviceID) } if c.pending == nil { t.Error("pending map is nil") } if c.done == nil { t.Error("done channel is nil") } if c.IsConnected() { t.Error("should not be connected before Connect()") } } // TestMsgTypeConstants verifies the wire-format type values match the // original enum (request=0, response=1, notification=2, reply=3). func TestMsgTypeConstants(t *testing.T) { cases := map[MsgType]uint32{ MsgRequest: 0, MsgResponse: 1, MsgNotification: 2, MsgReply: 3, } for mt, expected := range cases { if uint32(mt) != expected { t.Errorf("MsgType %d: got value %d, want %d", mt, uint32(mt), expected) } } } // TestSubscribeNotifications verifies the notification fan-out works: // multiple subscribers all receive the same packet. func TestSubscribeNotifications(t *testing.T) { c := NewClient("test", 1) ch1 := c.SubscribeNotifications() ch2 := c.SubscribeNotifications() pkt := &pb.WsPacket{Cmd: 15243, Type: uint32(MsgNotification)} c.dispatchNotification(pkt) // Both channels should receive a copy select { case got := <-ch1: if got.GetCmd() != 15243 { t.Errorf("ch1 cmd: got %d, want 15243", got.GetCmd()) } case <-time.After(100 * time.Millisecond): t.Fatal("ch1 did not receive notification") } select { case got := <-ch2: if got.GetCmd() != 15243 { t.Errorf("ch2 cmd: got %d, want 15243", got.GetCmd()) } case <-time.After(100 * time.Millisecond): t.Fatal("ch2 did not receive notification") } } // TestEmptyDataEnvelope verifies that commands with no payload (empty Data) // survive a round-trip. func TestEmptyDataEnvelope(t *testing.T) { original := &pb.WsPacket{ MajorVersion: WsMajorVersion, MinorVersion: WsMinorVersion, DeviceId: 1, ModuleId: 3, // MODULE_ASTRO Cmd: 11000, Type: uint32(MsgRequest), Data: nil, ClientId: "dwarfctl", } raw, err := proto.Marshal(original) if err != nil { t.Fatalf("marshal: %v", err) } decoded := &pb.WsPacket{} if err := proto.Unmarshal(raw, decoded); err != nil { t.Fatalf("unmarshal: %v", err) } if decoded.GetCmd() != 11000 { t.Errorf("cmd: got %d, want 11000", decoded.GetCmd()) } if len(decoded.GetData()) != 0 { t.Errorf("data should be empty, got %d bytes", len(decoded.GetData())) } } // TestWsPort verifies the port constant matches the reverse-engineered value. func TestWsPort(t *testing.T) { if WsPort != 9900 { t.Errorf("WsPort: got %d, want 9900", WsPort) } } // TestVersionConstants verifies the protocol version constants. func TestVersionConstants(t *testing.T) { if WsMajorVersion != 2 { t.Errorf("WsMajorVersion: got %d, want 2", WsMajorVersion) } if WsMinorVersion != 3 { t.Errorf("WsMinorVersion: got %d, want 3", WsMinorVersion) } }