protoconv_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close