1package ports
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "reflect"
8 "testing"
9
10 "github.com/bnema/vev/internal/domain"
11)
12
13func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) {
14 if ProtocolVersion != 29 {
15 t.Fatalf("ProtocolVersion = %d, want 29", ProtocolVersion)
16 }
17 payload := MarshalHello(Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}})
18 if len(payload) < 2 {
19 t.Fatal("MarshalHello produced a truncated valid payload")
20 }
21 binary.BigEndian.PutUint16(payload, 21)
22 if _, err := UnmarshalHello(payload); !errors.Is(err, ErrInvalidHello) {
23 t.Fatalf("UnmarshalHello(v21) error = %v, want ErrInvalidHello", err)
24 }
25}
26
27func TestFinalHelloGoldenStrict(t *testing.T) {
28 msg := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
29 want := append([]byte{0, 29, 2}, make([]byte, 16+8)...)
30 want = append(want, 0, 0, 0, 80, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
31 got := MarshalHello(msg)
32 if !bytes.Equal(got, want) {
33 t.Fatalf("Hello bytes = %x, want %x", got, want)
34 }
35 back, err := UnmarshalHello(got)
36 if err != nil {
37 t.Fatalf("UnmarshalHello() error = %v", err)
38 }
39 if !reflect.DeepEqual(back, msg) {
40 t.Fatalf("Hello = %+v, want %+v", back, msg)
41 }
42 assertAllPrefixesFail(t, got, UnmarshalHello)
43 assertTrailingGarbageFails(t, got, UnmarshalHello)
44}
45
46func TestFinalHelloSemanticValidation(t *testing.T) {
47 valid := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
48 for _, tt := range []struct {
49 name string
50 mutate func(*Hello)
51 }{
52 {name: "intent", mutate: func(h *Hello) { h.Intent = 99 }},
53 {name: "zero columns", mutate: func(h *Hello) { h.Size.Cols = 0 }},
54 {name: "zero rows", mutate: func(h *Hello) { h.Size.Rows = 0 }},
55 {name: "area", mutate: func(h *Hello) { h.Size = domain.Size{Cols: 513, Rows: 512} }},
56 } {
57 t.Run(tt.name, func(t *testing.T) {
58 msg := valid
59 tt.mutate(&msg)
60 if err := ValidateHello(msg); err == nil {
61 t.Fatal("ValidateHello accepted malformed semantics")
62 }
63 })
64 }
65}
66
67func TestFinalResizeGoldenStrict(t *testing.T) {
68 msg := Resize{Size: domain.Size{Cols: 80, Rows: 24}}
69 want := []byte{0, 80, 0, 24}
70 got, err := MarshalResize(msg)
71 if err != nil {
72 t.Fatalf("MarshalResize() error = %v", err)
73 }
74 if !bytes.Equal(got, want) {
75 t.Fatalf("Resize bytes = %x, want %x", got, want)
76 }
77 back, err := UnmarshalResize(got)
78 if err != nil || back != msg {
79 t.Fatalf("Resize = %+v, error %v, want %+v", back, err, msg)
80 }
81 assertAllPrefixesFail(t, got, UnmarshalResize)
82 assertTrailingGarbageFails(t, got, UnmarshalResize)
83 for _, size := range []domain.Size{{}, {Cols: 513, Rows: 512}} {
84 if got, err := MarshalResize(Resize{Size: size}); err == nil || got != nil {
85 t.Fatalf("MarshalResize(%+v) = (%x, %v), want nil payload and error", size, got, err)
86 }
87 }
88}
89
90func TestFinalOutputGoldenStrict(t *testing.T) {
91 msg := Output{
92 Epoch: 1, Base: 0, New: 7, Echo: 8, ViewRevision: 9,
93 Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("ok"),
94 }
95 want := []byte{
96 0, 0, 0, 0, 0, 0, 0, 1,
97 0, 0, 0, 0, 0, 0, 0, 0,
98 0, 0, 0, 0, 0, 0, 0, 7,
99 0, 0, 0, 0, 0, 0, 0, 8,
100 0, 0, 0, 0, 0, 0, 0, 9,
101 0, 80, 0, 24, 1,
102 0, 0, 0, 2, 'o', 'k',
103 }
104 got, err := MarshalOutput(msg)
105 if err != nil {
106 t.Fatalf("MarshalOutput() error = %v", err)
107 }
108 if !bytes.Equal(got, want) {
109 t.Fatalf("Output bytes = %x, want %x", got, want)
110 }
111 back, err := UnmarshalOutput(got)
112 if err != nil || back.Epoch != msg.Epoch || back.Base != msg.Base || back.New != msg.New || back.Echo != msg.Echo || back.ViewRevision != msg.ViewRevision || back.Size != msg.Size || back.Full != msg.Full || !bytes.Equal(back.Data, msg.Data) {
113 t.Fatalf("Output = %+v, error %v, want %+v", back, err, msg)
114 }
115 assertAllPrefixesFail(t, got, UnmarshalOutput)
116 assertTrailingGarbageFails(t, got, UnmarshalOutput)
117}
118
119func TestFinalOutputSemanticValidationBeforeDataAllocation(t *testing.T) {
120 valid := Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true}
121 for _, bad := range []Output{
122 {},
123 {Epoch: 1, Base: 0, New: 1, Size: valid.Size},
124 {Epoch: 1, Base: 0, New: 1, Full: true},
125 } {
126 if got, err := MarshalOutput(bad); err == nil || got != nil {
127 t.Fatalf("MarshalOutput(%+v) = (%x, %v), want nil payload and error", bad, got, err)
128 }
129 }
130 payload, err := MarshalOutput(valid)
131 if err != nil {
132 t.Fatal(err)
133 }
134 for _, tt := range []struct {
135 name string
136 mutate func([]byte)
137 }{
138 {name: "zero epoch", mutate: func(b []byte) { b[7] = 0 }},
139 {name: "new does not advance", mutate: func(b []byte) { binary.BigEndian.PutUint64(b[16:24], 0) }},
140 {name: "size zero", mutate: func(b []byte) { binary.BigEndian.PutUint16(b[40:42], 0) }},
141 {name: "full flag false for reset", mutate: func(b []byte) { b[44] = 0 }},
142 {name: "invalid bool", mutate: func(b []byte) { b[44] = 2 }},
143 {name: "impossible data length", mutate: func(b []byte) { binary.BigEndian.PutUint32(b[45:49], ^uint32(0)) }},
144 } {
145 t.Run(tt.name, func(t *testing.T) {
146 bad := append([]byte(nil), payload...)
147 tt.mutate(bad)
148 if _, err := UnmarshalOutput(bad); err == nil {
149 t.Fatal("UnmarshalOutput accepted malformed semantics")
150 }
151 })
152 }
153}
154
155func TestFinalAckGoldenStrict(t *testing.T) {
156 msg := Ack{Epoch: 2, State: 7}
157 want := []byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 7}
158 got, err := MarshalAck(msg)
159 if err != nil {
160 t.Fatalf("MarshalAck() error = %v", err)
161 }
162 if !bytes.Equal(got, want) {
163 t.Fatalf("Ack bytes = %x, want %x", got, want)
164 }
165 back, err := UnmarshalAck(got)
166 if err != nil || back != (Ack{Epoch: 2, State: 7}) {
167 t.Fatalf("Ack = %+v, error %v", back, err)
168 }
169 assertAllPrefixesFail(t, got, UnmarshalAck)
170 assertTrailingGarbageFails(t, got, UnmarshalAck)
171 if _, err := UnmarshalAck(make([]byte, 16)); err == nil {
172 t.Fatal("UnmarshalAck accepted zero epoch")
173 }
174 if got, err := MarshalAck(Ack{State: 7}); err == nil || got != nil {
175 t.Fatalf("MarshalAck accepted zero epoch: payload=%x err=%v", got, err)
176 }
177}
178
179func TestFinalAttachTargetGoldenStrict(t *testing.T) {
180 msg := AttachTarget{Endpoint: "host", Session: "work", Intent: IntentAttach}
181 want := []byte{0, 4, 'h', 'o', 's', 't', 0, 4, 'w', 'o', 'r', 'k', 2, 0, 0}
182 got := MarshalAttachTarget(msg)
183 if !bytes.Equal(got, want) {
184 t.Fatalf("AttachTarget bytes = %x, want %x", got, want)
185 }
186 back, err := UnmarshalAttachTarget(got)
187 if err != nil || back != msg {
188 t.Fatalf("AttachTarget = %+v, error %v, want %+v", back, err, msg)
189 }
190 assertAllPrefixesFail(t, got, UnmarshalAttachTarget)
191 assertTrailingGarbageFails(t, got, UnmarshalAttachTarget)
192 for _, bad := range []AttachTarget{{Session: "work", Intent: IntentAttach}, {Endpoint: "host", Intent: IntentAttach}, {Endpoint: "host", Session: "work", Intent: 99}} {
193 if got := MarshalAttachTarget(bad); got != nil {
194 t.Fatalf("MarshalAttachTarget(%+v) = %x, want nil", bad, got)
195 }
196 }
197}
198
199func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
200 tests := []struct {
201 name string
202 payload []byte
203 decode func([]byte) error
204 }{
205 {
206 name: "hello boolean",
207 payload: func() []byte {
208 b := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})
209 b[len(b)-8] = 2
210 return b
211 }(),
212 decode: func(b []byte) error { _, err := UnmarshalHello(b); return err },
213 },
214 {
215 name: "welcome boolean",
216 payload: func() []byte {
217 b := MarshalWelcome(Welcome{SessionID: "id"})
218 b[6] = 2
219 return b
220 }(),
221 decode: func(b []byte) error { _, err := UnmarshalWelcome(b); return err },
222 },
223 {name: "detached reason", payload: []byte{0xff}, decode: func(b []byte) error { _, err := UnmarshalDetached(b); return err }},
224 {
225 name: "kill boolean",
226 payload: func() []byte {
227 b := MarshalKill(Kill{All: true})
228 b[len(b)-1] = 2
229 return b
230 }(),
231 decode: func(b []byte) error { _, err := UnmarshalKill(b); return err },
232 },
233 {
234 name: "sessions ephemeral boolean",
235 payload: func() []byte {
236 b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
237 b[6] = 2
238 return b
239 }(),
240 decode: func(b []byte) error { _, err := UnmarshalSessions(b); return err },
241 },
242 {
243 name: "sessions attached boolean",
244 payload: func() []byte {
245 b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
246 b[9] = 2
247 return b
248 }(),
249 decode: func(b []byte) error { _, err := UnmarshalSessions(b); return err },
250 },
251 {name: "theme flags", payload: func() []byte { b := make([]byte, 57); b[0] = 0x20; return b }(), decode: func(b []byte) error { _, err := UnmarshalTheme(b); return err }},
252 }
253 for _, tt := range tests {
254 t.Run(tt.name, func(t *testing.T) {
255 if err := tt.decode(tt.payload); err == nil {
256 t.Fatal("decoder accepted unknown closed wire value")
257 }
258 })
259 }
260}