1package protoconv
2
3import (
4 "errors"
5 "testing"
6
7 renderer "github.com/bnema/vev-vt"
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)
13
14// TestRangeHelpers proves Uint8/Uint16 accept every in-range value and
15// refuse every value whose high bits would otherwise truncate into an
16// aliased in-range value.
17func TestRangeHelpers(t *testing.T) {
18 t.Run("Uint8", func(t *testing.T) {
19 tests := []struct {
20 name string
21 in uint32
22 want uint8
23 wantErr bool
24 }{
25 {"zero", 0, 0, false},
26 {"max", 255, 255, false},
27 {"overflow by one", 256, 0, true},
28 {"far overflow", 1 << 20, 0, true},
29 }
30 for _, tt := range tests {
31 t.Run(tt.name, func(t *testing.T) {
32 got, err := Uint8[uint8](tt.in)
33 if tt.wantErr {
34 require.ErrorIs(t, err, ErrOutOfRange)
35 return
36 }
37 require.NoError(t, err)
38 require.Equal(t, tt.want, got)
39 })
40 }
41 })
42
43 t.Run("Uint16", func(t *testing.T) {
44 tests := []struct {
45 name string
46 in uint32
47 want uint16
48 wantErr bool
49 }{
50 {"zero", 0, 0, false},
51 {"max", 65535, 65535, false},
52 {"overflow by one", 65536, 0, true},
53 {"aliasing overflow", 1<<16 + 1, 0, true},
54 }
55 for _, tt := range tests {
56 t.Run(tt.name, func(t *testing.T) {
57 got, err := Uint16[uint16](tt.in)
58 if tt.wantErr {
59 require.ErrorIs(t, err, ErrOutOfRange)
60 return
61 }
62 require.NoError(t, err)
63 require.Equal(t, tt.want, got)
64 })
65 }
66 })
67
68 t.Run("Uint8 instantiated over a named enum", func(t *testing.T) {
69 got, err := Uint8[domain.TabSelectorKind](uint32(domain.TabSelectorByStableID))
70 require.NoError(t, err)
71 require.Equal(t, domain.TabSelectorByStableID, got)
72 _, err = Uint8[domain.TabSelectorKind](256)
73 require.ErrorIs(t, err, ErrOutOfRange)
74 })
75}
76
77// TestLifecycleRoundTrip proves lifecycle IDs survive an encode/decode
78// cycle byte for byte and that decode refuses a nil message or a
79// wrong-length value.
80func TestLifecycleRoundTrip(t *testing.T) {
81 id := domain.SessionLifecycleID{1, 2, 3, 4}
82 wireID := LifecycleToWire(id)
83 require.Equal(t, id[:], wireID.GetValue())
84
85 decoded, err := LifecycleFromWire(wireID)
86 require.NoError(t, err)
87 require.Equal(t, id, decoded)
88
89 t.Run("nil message refused", func(t *testing.T) {
90 _, err := LifecycleFromWire(nil)
91 require.ErrorIs(t, err, ErrOutOfRange)
92 })
93
94 t.Run("wrong length refused", func(t *testing.T) {
95 _, err := LifecycleFromWire(&wire.LifecycleID{Value: []byte{1, 2, 3}})
96 require.ErrorIs(t, err, ErrOutOfRange)
97 })
98}
99
100// TestTabSelectorRoundTrip proves a tab selector survives an encode/decode
101// cycle, that a nil message decodes as the zero selector (it composes into
102// an optional stopped-tab field), and that every narrowing field is
103// range-checked.
104func TestTabSelectorRoundTrip(t *testing.T) {
105 selector := domain.TabSelector{
106 Kind: domain.TabSelectorByStableID, StableID: "tab-1",
107 Ordinal: 3, RawName: "work", ExpectedCount: 4,
108 }
109 wireSelector := TabSelectorToWire(selector)
110 decoded, err := TabSelectorFromWire(wireSelector)
111 require.NoError(t, err)
112 require.Equal(t, selector, decoded)
113
114 t.Run("nil message tolerated", func(t *testing.T) {
115 decoded, err := TabSelectorFromWire(nil)
116 require.NoError(t, err)
117 require.Equal(t, domain.TabSelector{}, decoded)
118 })
119
120 tests := []struct {
121 name string
122 msg *wire.TabSelector
123 }{
124 {"kind out of range", &wire.TabSelector{Kind: 256}},
125 {"ordinal out of range", &wire.TabSelector{Ordinal: 1 << 16}},
126 {"expected count out of range", &wire.TabSelector{ExpectedCount: 1 << 16}},
127 }
128 for _, tt := range tests {
129 t.Run(tt.name, func(t *testing.T) {
130 _, err := TabSelectorFromWire(tt.msg)
131 require.ErrorIs(t, err, ErrOutOfRange)
132 })
133 }
134}
135
136func testRemoteTarget() domain.RemoteSessionTarget {
137 return domain.RemoteSessionTarget{
138 Endpoint: "dev@host:22", DisplayOrigin: "dev@host",
139 LifecycleID: domain.SessionLifecycleID{5, 6, 7},
140 SessionName: "work", LiveTabID: "tab-1",
141 }
142}
143
144// TestRemoteTargetRoundTrip proves a remote target survives an
145// encode/decode cycle and that decode refuses a nil message and every
146// nested narrowing failure.
147func TestRemoteTargetRoundTrip(t *testing.T) {
148 target := testRemoteTarget()
149 wireTarget := RemoteTargetToWire(target)
150 decoded, err := RemoteTargetFromWire(wireTarget)
151 require.NoError(t, err)
152 require.Equal(t, target, decoded)
153
154 t.Run("nil message refused", func(t *testing.T) {
155 _, err := RemoteTargetFromWire(nil)
156 require.ErrorIs(t, err, ErrOutOfRange)
157 })
158
159 t.Run("invalid lifecycle refused", func(t *testing.T) {
160 broken := RemoteTargetToWire(target)
161 broken.LifecycleId = &wire.LifecycleID{Value: []byte{1}}
162 _, err := RemoteTargetFromWire(broken)
163 require.ErrorIs(t, err, ErrOutOfRange)
164 })
165
166 t.Run("invalid stopped tab refused", func(t *testing.T) {
167 broken := RemoteTargetToWire(target)
168 broken.StoppedTab.Kind = 256
169 _, err := RemoteTargetFromWire(broken)
170 require.ErrorIs(t, err, ErrOutOfRange)
171 })
172}
173
174// TestRGBRoundTrip proves an RGB color survives an encode/decode cycle,
175// that a nil message decodes as the zero color, and every component is
176// range-checked.
177func TestRGBRoundTrip(t *testing.T) {
178 color := renderer.RGB{R: 10, G: 20, B: 30}
179 wireColor := RGBToWire(color)
180 decoded, err := RGBFromWire(wireColor)
181 require.NoError(t, err)
182 require.Equal(t, color, decoded)
183
184 t.Run("nil message tolerated", func(t *testing.T) {
185 decoded, err := RGBFromWire(nil)
186 require.NoError(t, err)
187 require.Equal(t, renderer.RGB{}, decoded)
188 })
189
190 tests := []struct {
191 name string
192 msg *wire.RGB
193 }{
194 {"red out of range", &wire.RGB{R: 256}},
195 {"green out of range", &wire.RGB{G: 256}},
196 {"blue out of range", &wire.RGB{B: 256}},
197 }
198 for _, tt := range tests {
199 t.Run(tt.name, func(t *testing.T) {
200 _, err := RGBFromWire(tt.msg)
201 require.ErrorIs(t, err, ErrOutOfRange)
202 })
203 }
204}
205
206func testStyle() renderer.Style {
207 return renderer.Style{
208 Bold: true, Italic: true, Attrs: renderer.AttrDim,
209 Foreground: 1, Background: 2,
210 UnderlineStyle: renderer.UnderlineSingle, UnderlineColor: 3, HasUnderlineColor: true,
211 ForegroundRGB: renderer.RGB{R: 1}, BackgroundRGB: renderer.RGB{G: 1}, UnderlineColorRGB: renderer.RGB{B: 1},
212 }
213}
214
215// TestCellStyleRoundTrip proves a cell style survives an encode/decode
216// cycle, that a nil message decodes as the zero style, and every narrowing
217// field is range-checked.
218func TestCellStyleRoundTrip(t *testing.T) {
219 style := testStyle()
220 wireStyle := CellStyleToWire(style)
221 decoded, err := CellStyleFromWire(wireStyle)
222 require.NoError(t, err)
223 require.Equal(t, style, decoded)
224
225 t.Run("nil message tolerated", func(t *testing.T) {
226 decoded, err := CellStyleFromWire(nil)
227 require.NoError(t, err)
228 require.Equal(t, renderer.Style{}, decoded)
229 })
230
231 tests := []struct {
232 name string
233 msg *wire.CellStyle
234 }{
235 {"foreground out of range", &wire.CellStyle{Foreground: 1 << 20}},
236 {"background out of range", &wire.CellStyle{Background: -(1 << 20)}},
237 {"underline color out of range", &wire.CellStyle{UnderlineColor: 1 << 20}},
238 {"underline style out of range", &wire.CellStyle{UnderlineStyle: 256}},
239 {"attrs out of range", &wire.CellStyle{Attrs: 1 << 16}},
240 {"foreground rgb out of range", &wire.CellStyle{ForegroundRgb: &wire.RGB{R: 256}}},
241 }
242 for _, tt := range tests {
243 t.Run(tt.name, func(t *testing.T) {
244 _, err := CellStyleFromWire(tt.msg)
245 require.ErrorIs(t, err, ErrOutOfRange)
246 })
247 }
248}
249
250// TestPreviewCellRoundTrip proves a cell survives an encode/decode cycle,
251// that decode refuses a nil message (unlike RGB/CellStyle, a single grid
252// cell is never optional), and that an out-of-range rune is refused in
253// both directions.
254func TestPreviewCellRoundTrip(t *testing.T) {
255 cell := renderer.Cell{Rune: 'A', Style: testStyle()}
256 wireCell, err := PreviewCellToWire(cell)
257 require.NoError(t, err)
258 decoded, err := PreviewCellFromWire(wireCell)
259 require.NoError(t, err)
260 require.Equal(t, cell, decoded)
261
262 t.Run("nil message refused", func(t *testing.T) {
263 _, err := PreviewCellFromWire(nil)
264 require.ErrorIs(t, err, ErrOutOfRange)
265 })
266
267 t.Run("encode rejects rune above max code point", func(t *testing.T) {
268 _, err := PreviewCellToWire(renderer.Cell{Rune: 0x110000})
269 require.ErrorIs(t, err, ErrOutOfRange)
270 })
271
272 t.Run("encode rejects negative rune", func(t *testing.T) {
273 _, err := PreviewCellToWire(renderer.Cell{Rune: -1})
274 require.ErrorIs(t, err, ErrOutOfRange)
275 })
276
277 t.Run("decode rejects rune above max code point", func(t *testing.T) {
278 _, err := PreviewCellFromWire(&wire.PreviewCell{RuneValue: 0x110000})
279 require.ErrorIs(t, err, ErrOutOfRange)
280 })
281}
282
283// TestPreviewCellsRoundTrip proves a slice of cells survives an
284// encode/decode cycle, that an empty/nil slice stays nil rather than an
285// empty allocation, and that one bad cell fails the whole slice.
286func TestPreviewCellsRoundTrip(t *testing.T) {
287 cells := []renderer.Cell{{Rune: 'a'}, {Rune: 'b'}}
288 wireCells, err := PreviewCellsToWire(cells)
289 require.NoError(t, err)
290 decoded, err := PreviewCellsFromWire(wireCells)
291 require.NoError(t, err)
292 require.Equal(t, cells, decoded)
293
294 t.Run("nil in, nil out", func(t *testing.T) {
295 out, err := PreviewCellsToWire(nil)
296 require.NoError(t, err)
297 require.Nil(t, out)
298 decoded, err := PreviewCellsFromWire(nil)
299 require.NoError(t, err)
300 require.Nil(t, decoded)
301 })
302
303 t.Run("bad cell fails the slice on encode", func(t *testing.T) {
304 _, err := PreviewCellsToWire([]renderer.Cell{{Rune: 'a'}, {Rune: 0x110000}})
305 require.ErrorIs(t, err, ErrOutOfRange)
306 })
307
308 t.Run("nil member fails the slice on decode", func(t *testing.T) {
309 _, err := PreviewCellsFromWire([]*wire.PreviewCell{{RuneValue: 'a'}, nil})
310 require.ErrorIs(t, err, ErrOutOfRange)
311 })
312}
313
314func testPreviewRequest() protocol.RemotePreviewRequest {
315 return protocol.RemotePreviewRequest{
316 Version: protocol.RemotePreviewSchemaVersion,
317 Target: testRemoteTarget(),
318 Width: 2, Height: 1,
319 }
320}
321
322// TestRemotePreviewRequestRoundTrip proves a remote preview request
323// survives an encode/decode cycle byte-for-byte (via re-marshal), that
324// decode refuses a nil message, and that both an invalid semantic value and
325// an out-of-range wire field are refused with the right error shape.
326func TestRemotePreviewRequestRoundTrip(t *testing.T) {
327 request := testPreviewRequest()
328 wireRequest, err := RemotePreviewRequestToWire(request)
329 require.NoError(t, err)
330 decoded, err := RemotePreviewRequestFromWire(wireRequest)
331 require.NoError(t, err)
332 require.Equal(t, request, decoded)
333
334 t.Run("nil message refused", func(t *testing.T) {
335 _, err := RemotePreviewRequestFromWire(nil)
336 require.ErrorIs(t, err, ErrOutOfRange)
337 })
338
339 t.Run("encode refuses invalid semantic value", func(t *testing.T) {
340 invalid := request
341 invalid.Width = 0
342 _, err := RemotePreviewRequestToWire(invalid)
343 require.ErrorIs(t, err, protocol.ErrInvalidRemotePreviewRequest)
344 })
345
346 t.Run("decode refuses out-of-range wire field", func(t *testing.T) {
347 broken, err := RemotePreviewRequestToWire(request)
348 require.NoError(t, err)
349 broken.Width = 1 << 16
350 _, err = RemotePreviewRequestFromWire(broken)
351 require.ErrorIs(t, err, ErrOutOfRange)
352 })
353
354 t.Run("decode re-validates the fully decoded request", func(t *testing.T) {
355 broken, err := RemotePreviewRequestToWire(request)
356 require.NoError(t, err)
357 broken.Width = 0
358 _, err = RemotePreviewRequestFromWire(broken)
359 require.ErrorIs(t, err, protocol.ErrInvalidRemotePreviewRequest)
360 })
361}
362
363func testRemotePreview() protocol.RemotePreview {
364 target := testRemoteTarget()
365 return protocol.RemotePreview{
366 Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
367 LifecycleID: target.LifecycleID, TabID: target.LiveTabID,
368 Revision: 11, Width: 2, Height: 1,
369 Cells: []renderer.Cell{{Rune: 'a'}, {Rune: 'b'}},
370 }
371}
372
373// TestRemotePreviewRoundTrip proves a remote preview survives an
374// encode/decode cycle, that decode refuses a nil message, and that a
375// too-large preview is refused with protocol.ErrRemotePreviewTooLarge on
376// both directions distinctly from an out-of-range wire field.
377func TestRemotePreviewRoundTrip(t *testing.T) {
378 preview := testRemotePreview()
379 wirePreview, err := RemotePreviewToWire(preview)
380 require.NoError(t, err)
381 decoded, err := RemotePreviewFromWire(wirePreview)
382 require.NoError(t, err)
383 require.Equal(t, preview, decoded)
384
385 t.Run("nil message refused", func(t *testing.T) {
386 _, err := RemotePreviewFromWire(nil)
387 require.ErrorIs(t, err, ErrOutOfRange)
388 })
389
390 t.Run("encode refuses a too-large preview", func(t *testing.T) {
391 invalid := preview
392 invalid.Cells = invalid.Cells[:1]
393 _, err := RemotePreviewToWire(invalid)
394 require.ErrorIs(t, err, protocol.ErrRemotePreviewTooLarge)
395 })
396
397 t.Run("decode refuses out-of-range wire field", func(t *testing.T) {
398 broken, err := RemotePreviewToWire(preview)
399 require.NoError(t, err)
400 broken.Status = 1 << 16
401 _, err = RemotePreviewFromWire(broken)
402 require.ErrorIs(t, err, ErrOutOfRange)
403 })
404
405 t.Run("decode refuses a too-large preview", func(t *testing.T) {
406 broken, err := RemotePreviewToWire(preview)
407 require.NoError(t, err)
408 broken.Cells = broken.Cells[:1]
409 _, err = RemotePreviewFromWire(broken)
410 require.ErrorIs(t, err, protocol.ErrRemotePreviewTooLarge)
411 })
412
413 t.Run("decode refuses cell encoding out of range", func(t *testing.T) {
414 broken, err := RemotePreviewToWire(preview)
415 require.NoError(t, err)
416 broken.Cells[0].RuneValue = 0x110000
417 _, err = RemotePreviewFromWire(broken)
418 require.ErrorIs(t, err, ErrOutOfRange)
419 })
420}
421
422// TestErrOutOfRangeIsDistinctFromProtocolSentinels proves protoconv never
423// aliases its own range sentinel with a protocol validation sentinel, since
424// callers (sessionwire, brokerwire) switch behavior on which one they see.
425func TestErrOutOfRangeIsDistinctFromProtocolSentinels(t *testing.T) {
426 require.False(t, errors.Is(ErrOutOfRange, protocol.ErrInvalidRemotePreview))
427 require.False(t, errors.Is(ErrOutOfRange, protocol.ErrInvalidRemotePreviewRequest))
428 require.False(t, errors.Is(ErrOutOfRange, protocol.ErrRemotePreviewTooLarge))
429}