v0.7.0 4c16dd8

proto_limits_category_test.go RawHistoryFolder

1package sessionwire
2
3import (
4	"bytes"
5	"strings"
6	"testing"
7
8	"google.golang.org/protobuf/encoding/protowire"
9	"google.golang.org/protobuf/proto"
10
11	"github.com/bnema/vev/internal/domain"
12	"github.com/bnema/vev/internal/protocol"
13	"github.com/bnema/vev/internal/protocol/wire"
14	"github.com/stretchr/testify/require"
15)
16
17// overControlCap is comfortably above wire.ControlEnvelopeLimit but well
18// below wire.AbsoluteEnvelopeLimit, so a category-exempt envelope still
19// rides the negotiated ceiling while a control envelope is capped.
20const overControlCap = wire.ControlEnvelopeLimit + (1 << 20)
21
22// testOutputRaw builds an Output that never compresses (Full is false), so
23// its serialized envelope stays above the control cap.
24func testOutputRaw(data []byte) protocol.Output {
25	return protocol.Output{Epoch: 1, New: 0, Size: domain.Size{Cols: 80, Rows: 24}, Data: data}
26}
27
28func mustMarshalClientEnvelope(t *testing.T, envelope *wire.ClientEnvelope) []byte {
29	t.Helper()
30	raw, err := proto.Marshal(envelope)
31	require.NoError(t, err)
32	return raw
33}
34
35func mustMarshalServerEnvelope(t *testing.T, envelope *wire.ServerEnvelope) []byte {
36	t.Helper()
37	raw, err := proto.Marshal(envelope)
38	require.NoError(t, err)
39	return raw
40}
41
42// TestEnvelopeCategoryClassification proves category detection derives from
43// the serialized leading tag only: ImagePush/Output are exempt, every other
44// variant and every unknown, empty, or malformed payload stays control.
45func TestEnvelopeCategoryClassification(t *testing.T) {
46	imagePayload := mustMarshalClientEnvelope(t, &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_ImagePush{ImagePush: &wire.ImagePush{Data: []byte("img")}}})
47	pingPayload := mustMarshalClientEnvelope(t, &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_Ping{Ping: &wire.Ping{}}})
48	outputPayload := mustMarshalServerEnvelope(t, &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_Output{Output: &wire.Output{Data: []byte("out")}}})
49	errorPayload := mustMarshalServerEnvelope(t, &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_Error{Error: &wire.ErrorMsg{}}})
50	unknownField := protowire.AppendBytes(protowire.AppendTag(nil, 99, protowire.BytesType), []byte("x"))
51
52	clientCases := []struct {
53		name    string
54		payload []byte
55		want    envelopeCategory
56	}{
57		{"image push is exempt", imagePayload, categoryBulk},
58		{"control variant", pingPayload, categoryControl},
59		{"server envelope is control", outputPayload, categoryControl},
60		{"unknown field", unknownField, categoryControl},
61		{"empty payload", nil, categoryControl},
62		{"truncated tag", []byte{0xFF}, categoryControl},
63		{"non-length-delimited tag", protowire.AppendTag(nil, 1, protowire.VarintType), categoryControl},
64	}
65	for _, tt := range clientCases {
66		t.Run("client/"+tt.name, func(t *testing.T) {
67			require.Equal(t, tt.want, clientEnvelopeCategory(tt.payload))
68		})
69	}
70
71	serverCases := []struct {
72		name    string
73		payload []byte
74		want    envelopeCategory
75	}{
76		{"output is exempt", outputPayload, categoryOutput},
77		{"control variant", errorPayload, categoryControl},
78		{"client envelope is control", imagePayload, categoryControl},
79		{"unknown field", unknownField, categoryControl},
80		{"empty payload", nil, categoryControl},
81		{"truncated tag", []byte{0xFF}, categoryControl},
82	}
83	for _, tt := range serverCases {
84		t.Run("server/"+tt.name, func(t *testing.T) {
85			require.Equal(t, tt.want, serverEnvelopeCategory(tt.payload))
86		})
87	}
88}
89
90// TestClientSendCategoryEnvelopeCeilings proves client sends cap control
91// envelopes at wire.ControlEnvelopeLimit while ImagePush rides the negotiated
92// ceiling; a lower negotiated ceiling still wins for both categories.
93func TestClientSendCategoryEnvelopeCeilings(t *testing.T) {
94	inputOver := protocol.Input{InputSeq: 1, Data: bytes.Repeat([]byte{0xAB}, overControlCap)}
95	imageOver := protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: bytes.Repeat([]byte{0xAB}, overControlCap)}
96	smallControl := protocol.Input{InputSeq: 1, Data: bytes.Repeat([]byte{0x5A}, 4096)}
97	smallLen := len(mustEncodeClient(t, smallControl))
98
99	tests := []struct {
100		name     string
101		message  protocol.ClientMessage
102		ceilings protoCeilings
103		wantErr  error
104	}{
105		{
106			name:     "input rides absolute negotiation",
107			message:  inputOver,
108			ceilings: defaultProtoCeilings(),
109		},
110		{
111			name:     "image rides absolute negotiation",
112			message:  imageOver,
113			ceilings: defaultProtoCeilings(),
114		},
115		{
116			name:     "image respects lower negotiated ceiling",
117			message:  imageOver,
118			ceilings: protoCeilings{maxReceiveEnvelopeBytes: wire.ControlEnvelopeLimit, outputDataLimit: uint64(protocol.MaxOutputDataLen)},
119			wantErr:  wire.ErrScanLength,
120		},
121		{
122			name:     "control fits negotiated lower ceiling",
123			message:  smallControl,
124			ceilings: protoCeilings{maxReceiveEnvelopeBytes: uint64(smallLen), outputDataLimit: uint64(protocol.MaxOutputDataLen)},
125		},
126		{
127			name:     "control above negotiated lower ceiling",
128			message:  smallControl,
129			ceilings: protoCeilings{maxReceiveEnvelopeBytes: uint64(smallLen - 1), outputDataLimit: uint64(protocol.MaxOutputDataLen)},
130			wantErr:  wire.ErrScanLength,
131		},
132	}
133	for _, tt := range tests {
134		t.Run(tt.name, func(t *testing.T) {
135			raw := &scriptedTransport{}
136			conn := &clientConnection{raw: raw, ceilings: tt.ceilings}
137			conn.preambleOnce.Do(func() {})
138			err := conn.SendClient(tt.message)
139			if tt.wantErr != nil {
140				require.ErrorIs(t, err, tt.wantErr)
141				require.Zero(t, raw.sentLen())
142				return
143			}
144			require.NoError(t, err)
145			require.Equal(t, 1, raw.sentLen())
146		})
147	}
148}
149
150// TestServerSendCategoryEnvelopeCeilings proves server sends cap control
151// envelopes at wire.ControlEnvelopeLimit while Output rides the negotiated
152// ceiling; a lower negotiated ceiling still wins for both categories.
153func TestServerSendCategoryEnvelopeCeilings(t *testing.T) {
154	controlOver := protocol.Sessions{Sessions: []protocol.SessionInfo{{Name: strings.Repeat("x", overControlCap)}}}
155	outputOver := testOutputRaw(bytes.Repeat([]byte{0xAB}, overControlCap))
156	smallControl := protocol.Sessions{Sessions: []protocol.SessionInfo{{Name: strings.Repeat("x", 4096)}}}
157	smallLen := len(mustEncodeServer(t, smallControl))
158
159	tests := []struct {
160		name     string
161		message  protocol.ServerMessage
162		ceilings protoCeilings
163		wantErr  error
164	}{
165		{
166			name:     "control capped even at absolute negotiation",
167			message:  controlOver,
168			ceilings: defaultProtoCeilings(),
169			wantErr:  wire.ErrScanLength,
170		},
171		{
172			name:     "output rides absolute negotiation",
173			message:  outputOver,
174			ceilings: defaultProtoCeilings(),
175		},
176		{
177			name:     "output respects lower negotiated ceiling",
178			message:  outputOver,
179			ceilings: protoCeilings{maxReceiveEnvelopeBytes: wire.ControlEnvelopeLimit, outputDataLimit: uint64(protocol.MaxOutputDataLen)},
180			wantErr:  wire.ErrScanLength,
181		},
182		{
183			name:     "control fits negotiated lower ceiling",
184			message:  smallControl,
185			ceilings: protoCeilings{maxReceiveEnvelopeBytes: uint64(smallLen), outputDataLimit: uint64(protocol.MaxOutputDataLen)},
186		},
187		{
188			name:     "control above negotiated lower ceiling",
189			message:  smallControl,
190			ceilings: protoCeilings{maxReceiveEnvelopeBytes: uint64(smallLen - 1), outputDataLimit: uint64(protocol.MaxOutputDataLen)},
191			wantErr:  wire.ErrScanLength,
192		},
193	}
194	for _, tt := range tests {
195		t.Run(tt.name, func(t *testing.T) {
196			raw := &scriptedTransport{}
197			conn := &serverConnection{raw: raw, ceilings: tt.ceilings}
198			err := conn.SendServer(tt.message)
199			if tt.wantErr != nil {
200				require.ErrorIs(t, err, tt.wantErr)
201				require.Zero(t, raw.sentLen())
202				return
203			}
204			require.NoError(t, err)
205			require.Equal(t, 1, raw.sentLen())
206		})
207	}
208}
209
210// TestServerSendModesShareControlCategoryCap proves async and owned
211// synchronous server sends enforce the same control cap as the normal path.
212func TestServerSendModesShareControlCategoryCap(t *testing.T) {
213	controlOver := protocol.Sessions{Sessions: []protocol.SessionInfo{{Name: strings.Repeat("x", overControlCap)}}}
214	raw := &asyncScriptedTransport{scriptedTransport: &scriptedTransport{}}
215	conn := &serverConnection{raw: raw, ceilings: defaultProtoCeilings()}
216
217	require.ErrorIs(t, conn.SendServerAsync(controlOver), wire.ErrScanLength)
218	require.ErrorIs(t, conn.SendServerSynchronous(controlOver), wire.ErrScanLength)
219	require.Empty(t, raw.async)
220	require.Empty(t, raw.synchronous)
221}
222
223// TestReceiveCategoryEnvelopeCeilings proves both typed receive paths apply
224// the same category ceilings as sends, including malformed and unknown
225// payloads, which must fall back to the strictest control ceiling.
226func TestReceiveCategoryEnvelopeCeilings(t *testing.T) {
227	serverInputOver := mustEncodeClient(t, protocol.Input{InputSeq: 1, Data: bytes.Repeat([]byte{0xAB}, overControlCap)})
228	clientImageOver := mustEncodeClient(t, protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: bytes.Repeat([]byte{0xAB}, overControlCap)})
229	clientControlOver := mustEncodeServer(t, protocol.Sessions{Sessions: []protocol.SessionInfo{{Name: strings.Repeat("x", overControlCap)}}})
230	clientOutputOver := mustEncodeServer(t, testOutputRaw(bytes.Repeat([]byte{0xAB}, overControlCap)))
231
232	oversizedMalformed := bytes.Repeat([]byte{0xFF}, overControlCap)
233	oversizedUnknown := protowire.AppendBytes(protowire.AppendTag(nil, 99, protowire.BytesType), bytes.Repeat([]byte{0xAB}, overControlCap))
234	smallUnknown := protowire.AppendBytes(protowire.AppendTag(nil, 99, protowire.BytesType), []byte("x"))
235
236	lowerControl := protoCeilings{maxReceiveEnvelopeBytes: wire.ControlEnvelopeLimit, outputDataLimit: uint64(protocol.MaxOutputDataLen)}
237
238	tests := []struct {
239		name     string
240		receiver string
241		payload  []byte
242		ceilings protoCeilings
243		wantErr  error
244		wantCat  protocol.DecodeCategory
245	}{
246		{
247			name:     "server receive input rides absolute negotiation",
248			receiver: "server",
249			payload:  serverInputOver,
250			ceilings: defaultProtoCeilings(),
251		},
252		{
253			name:     "server receive image rides absolute negotiation",
254			receiver: "server",
255			payload:  clientImageOver,
256			ceilings: defaultProtoCeilings(),
257		},
258		{
259			name:     "server receive image respects lower negotiated ceiling",
260			receiver: "server",
261			payload:  clientImageOver,
262			ceilings: lowerControl,
263			wantErr:  wire.ErrScanLength,
264			wantCat:  protocol.DecodeMalformed,
265		},
266		{
267			name:     "server receive oversized malformed defaults to control",
268			receiver: "server",
269			payload:  oversizedMalformed,
270			ceilings: defaultProtoCeilings(),
271			wantErr:  wire.ErrScanLength,
272			wantCat:  protocol.DecodeMalformed,
273		},
274		{
275			name:     "server receive oversized unknown field defaults to control",
276			receiver: "server",
277			payload:  oversizedUnknown,
278			ceilings: defaultProtoCeilings(),
279			wantErr:  wire.ErrScanLength,
280			wantCat:  protocol.DecodeMalformed,
281		},
282		{
283			name:     "server receive small unknown field stays unknown type",
284			receiver: "server",
285			payload:  smallUnknown,
286			ceilings: defaultProtoCeilings(),
287			wantErr:  wire.ErrScanUnknown,
288			wantCat:  protocol.DecodeUnknownType,
289		},
290		{
291			name:     "client receive control capped at absolute negotiation",
292			receiver: "client",
293			payload:  clientControlOver,
294			ceilings: defaultProtoCeilings(),
295			wantErr:  wire.ErrScanLength,
296			wantCat:  protocol.DecodeMalformed,
297		},
298		{
299			name:     "client receive output rides absolute negotiation",
300			receiver: "client",
301			payload:  clientOutputOver,
302			ceilings: defaultProtoCeilings(),
303		},
304		{
305			name:     "client receive output respects lower negotiated ceiling",
306			receiver: "client",
307			payload:  clientOutputOver,
308			ceilings: lowerControl,
309			wantErr:  wire.ErrScanLength,
310			wantCat:  protocol.DecodeMalformed,
311		},
312		{
313			name:     "client receive oversized malformed defaults to control",
314			receiver: "client",
315			payload:  oversizedMalformed,
316			ceilings: defaultProtoCeilings(),
317			wantErr:  wire.ErrScanLength,
318			wantCat:  protocol.DecodeMalformed,
319		},
320		{
321			name:     "client receive small unknown field stays unknown type",
322			receiver: "client",
323			payload:  smallUnknown,
324			ceilings: defaultProtoCeilings(),
325			wantErr:  wire.ErrScanUnknown,
326			wantCat:  protocol.DecodeUnknownType,
327		},
328	}
329	for _, tt := range tests {
330		t.Run(tt.name, func(t *testing.T) {
331			raw := &scriptedTransport{recv: []wire.Envelope{{Payload: tt.payload}}}
332			var err error
333			if tt.receiver == "server" {
334				conn := &serverConnection{raw: raw, ceilings: tt.ceilings}
335				conn.preambleOnce.Do(func() {})
336				_, err = conn.ReceiveClient()
337			} else {
338				conn := &clientConnection{raw: raw, ceilings: tt.ceilings}
339				conn.preambleOnce.Do(func() {})
340				_, err = conn.ReceiveServer()
341			}
342			if tt.wantErr == nil {
343				require.NoError(t, err)
344				return
345			}
346			var failure *protocol.DecodeFailure
347			require.ErrorAs(t, err, &failure)
348			require.ErrorIs(t, failure.Err, tt.wantErr)
349			require.Equal(t, tt.wantCat, failure.Category)
350		})
351	}
352}

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