1package sessionwire
2
3import (
4 "encoding/hex"
5 "reflect"
6 "testing"
7
8 "github.com/bnema/vev/internal/domain"
9 "github.com/bnema/vev/internal/protocol"
10 "github.com/bnema/vev/internal/protocol/wire"
11 "github.com/stretchr/testify/require"
12 "google.golang.org/protobuf/proto"
13 "google.golang.org/protobuf/reflect/protoreflect"
14)
15
16func TestAttachmentEnvelopes(t *testing.T) {
17 target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
18 for _, tt := range []struct {
19 name string
20 message any
21 golden string
22 }{
23 {"suspend", protocol.SuspendAttachment{RequestID: 1}, "ea01020801"},
24 {"suspended", protocol.AttachmentSuspended{RequestID: 1, Target: target}, "fa011e0801121a0a120a10010000000000000000000000000000001204776f726b"},
25 {"activate", protocol.ActivateAttachment{RequestID: 2, Target: target, Size: domain.Size{Cols: 80, Rows: 24}}, "f201220802121a0a120a10010000000000000000000000000000001204776f726b18502018"},
26 {"activated", protocol.AttachmentActivated{RequestID: 2, Identity: protocol.CommittedRouteIdentity{Target: target}, Epoch: 2, State: 1, ViewPublication: 3}, "8202260802121c0a1a0a120a10010000000000000000000000000000001204776f726b180220012803"},
27 } {
28 t.Run(tt.name, func(t *testing.T) {
29 var raw []byte
30 var err error
31 var decode func([]byte) (any, error)
32 var wrong func([]byte) (any, error)
33 if m, ok := tt.message.(protocol.ClientMessage); ok {
34 raw, err = EncodeClientMessage(m)
35 decode = func(b []byte) (any, error) { return DecodeClientEnvelope(b) }
36 wrong = func(b []byte) (any, error) { return DecodeServerEnvelope(b) }
37 } else {
38 raw, err = EncodeServerMessage(tt.message.(protocol.ServerMessage))
39 decode = func(b []byte) (any, error) { return DecodeServerEnvelope(b) }
40 wrong = func(b []byte) (any, error) { return DecodeClientEnvelope(b) }
41 }
42 require.NoError(t, err)
43 require.Equal(t, tt.golden, hex.EncodeToString(raw))
44 got, err := decode(raw)
45 require.NoError(t, err)
46 require.Equal(t, tt.message, got)
47 _, err = wrong(raw)
48 require.Error(t, err)
49 for i := 0; i < len(raw); i++ {
50 _, err = decode(raw[:i])
51 require.Error(t, err, "prefix %d", i)
52 }
53 for _, tail := range [][]byte{{0}, {0x80}, raw, {0xf8, 0x07, 0x01}} {
54 _, err = decode(append(append([]byte(nil), raw...), tail...))
55 require.Error(t, err)
56 }
57 })
58 }
59}
60
61// Clearing any required field must fail on both conversion directions. Direct
62// protobuf marshal deliberately bypasses the semantic encoder for hostile input.
63func TestAttachmentRequiredFields(t *testing.T) {
64 target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
65 for _, message := range []any{
66 protocol.SuspendAttachment{RequestID: 1},
67 protocol.AttachmentSuspended{RequestID: 1, Target: target},
68 protocol.ActivateAttachment{RequestID: 2, Target: target, Size: domain.Size{Cols: 80, Rows: 24}},
69 protocol.AttachmentActivated{RequestID: 2, Identity: protocol.CommittedRouteIdentity{Target: target}, Epoch: 2, State: 1, ViewPublication: 3},
70 } {
71 t.Run(reflect.TypeOf(message).Name(), func(t *testing.T) {
72 value := reflect.ValueOf(message)
73 // Pointer forms share the same validated path; typed nil pointers fail.
74 pointer := reflect.New(value.Type())
75 pointer.Elem().Set(value)
76 nilPointer := reflect.Zero(pointer.Type()).Interface()
77 encode := func(v any) ([]byte, error) {
78 if m, ok := v.(protocol.ClientMessage); ok {
79 return EncodeClientMessage(m)
80 }
81 return EncodeServerMessage(v.(protocol.ServerMessage))
82 }
83 _, err := encode(pointer.Interface())
84 require.NoError(t, err)
85 _, err = encode(nilPointer)
86 require.Error(t, err)
87 for i := 0; i < value.NumField(); i++ {
88 if value.Type().Field(i).Name == "PixelWidth" || value.Type().Field(i).Name == "PixelHeight" {
89 continue
90 }
91 bad := reflect.New(value.Type()).Elem()
92 bad.Set(value)
93 bad.Field(i).SetZero()
94 _, err = encode(bad.Interface())
95 require.Error(t, err, value.Type().Field(i).Name)
96 }
97 var envelope proto.Message
98 var decode func([]byte) (any, error)
99 if m, ok := message.(protocol.ClientMessage); ok {
100 envelope, err = encodeProtoClient(m)
101 decode = func(b []byte) (any, error) { return DecodeClientEnvelope(b) }
102 } else {
103 envelope, err = encodeProtoServer(message.(protocol.ServerMessage))
104 decode = func(b []byte) (any, error) { return DecodeServerEnvelope(b) }
105 }
106 require.NoError(t, err)
107 descriptor := envelope.ProtoReflect().Descriptor().Oneofs().Get(0)
108 variant := envelope.ProtoReflect().WhichOneof(descriptor)
109 payload := envelope.ProtoReflect().Get(variant).Message()
110 payload.Range(func(field protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
111 clone := proto.Clone(envelope)
112 clone.ProtoReflect().Get(variant).Message().Clear(field)
113 raw, err := proto.Marshal(clone)
114 require.NoError(t, err)
115 _, err = decode(raw)
116 require.Error(t, err, string(field.Name()))
117 return true
118 })
119 })
120 }
121}
122
123func TestAttachmentActivationRejectsMalformedGeometryAndIdentity(t *testing.T) {
124 target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
125 for _, mutate := range []func(*wire.ActivateAttachment){
126 func(m *wire.ActivateAttachment) { m.Cols = 1 << 16 },
127 func(m *wire.ActivateAttachment) { m.Rows = 1 << 16 },
128 func(m *wire.ActivateAttachment) { m.PixelWidth = 1 },
129 func(m *wire.ActivateAttachment) { m.PixelWidth = 1 << 16; m.PixelHeight = 1 },
130 func(m *wire.ActivateAttachment) { m.Target.LifecycleId.Value = []byte{1} },
131 func(m *wire.ActivateAttachment) { m.Target.SessionName = "" },
132 } {
133 m := &wire.ActivateAttachment{RequestId: 2, Target: exactTargetToWire(&target), Cols: 80, Rows: 24}
134 mutate(m)
135 raw, err := proto.Marshal(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_ActivateAttachment{ActivateAttachment: m}})
136 require.NoError(t, err)
137 _, err = DecodeClientEnvelope(raw)
138 require.Error(t, err)
139 }
140}
141
142// This is a typed connection contract test, not a daemon lifecycle test. The
143// scripted peer supplies the barrier and full-publication ordering Phase 2
144// must enforce in the real daemon.
145func TestAttachmentTypedConnectionSequence(t *testing.T) {
146 context := testUIOutputContext()
147 target := context.Route.Target
148 suspend := protocol.SuspendAttachment{RequestID: 10}
149 suspended := protocol.AttachmentSuspended{RequestID: 10, Target: target}
150 activate := protocol.ActivateAttachment{RequestID: 11, Target: target, Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480}
151 full := protocol.Output{Epoch: 2, New: 1, Full: true, Size: activate.Size, Context: context, Data: []byte("full")}
152 activated := protocol.AttachmentActivated{RequestID: 11, Identity: context.Route, Epoch: full.Epoch, State: full.New, ViewPublication: context.Publication}
153 clientRaw := &scriptedTransport{recv: []wire.Envelope{mustPreambleResponse(t)}}
154 serverRaw := &scriptedTransport{recv: []wire.Envelope{mustPreambleRequest(t)}}
155 client := NewClientConnection(clientRaw)
156 server := NewServerConnection(serverRaw)
157 t.Cleanup(func() { require.NoError(t, client.Close()); require.NoError(t, server.Close()) })
158 sendClient := func(message protocol.ClientMessage) {
159 require.NoError(t, client.SendClient(message))
160 serverRaw.recv = append(serverRaw.recv, wire.Envelope{Payload: clientRaw.sentPayload(clientRaw.sentLen() - 1)})
161 got, err := server.ReceiveClient()
162 require.NoError(t, err)
163 require.Equal(t, message, got)
164 }
165 sendServer := func(message protocol.ServerMessage) {
166 require.NoError(t, server.SendServer(message))
167 clientRaw.recv = append(clientRaw.recv, wire.Envelope{Payload: serverRaw.sentPayload(serverRaw.sentLen() - 1)})
168 got, err := client.ReceiveServer()
169 require.NoError(t, err)
170 require.Equal(t, message, got)
171 }
172 sendClient(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: target.SessionName, ExactTarget: &target, Size: activate.Size})
173 sendServer(protocol.Welcome{SessionID: "session", SessionName: target.SessionName, CommittedIdentity: &context.Route})
174 initial := full
175 initial.Epoch = 1
176 sendServer(initial)
177 sendClient(suspend)
178 sendServer(suspended)
179 sendClient(protocol.Ping{})
180 sendServer(protocol.Pong{})
181 sendClient(activate)
182 sendServer(full)
183 sendServer(activated)
184 require.NotEqual(t, initial.Epoch, activated.Epoch)
185 require.Equal(t, activate.RequestID, activated.RequestID)
186 require.Equal(t, activate.Target, activated.Identity.Target)
187 sendClient(protocol.Input{InputSeq: 1, Data: []byte("x")})
188}