main ece7682

kill_result_test.go RawHistoryFolder

1package sessionwire
2
3// Kill/KillResult wire contract: a Kill request carries a unique nonzero
4// RequestID, exactly one correlated KillResult answers it, and both directions
5// fail closed on malformed, wrong-direction, truncated, trailing, or oversize
6// bytes. The result is a bounded control envelope: it never rides the
7// Output/bulk ceiling exemption.
8
9import (
10	"testing"
11
12	"google.golang.org/protobuf/proto"
13
14	"github.com/bnema/vev/internal/protocol"
15	"github.com/bnema/vev/internal/protocol/wire"
16	"github.com/stretchr/testify/require"
17)
18
19func testKillResult() protocol.KillResult {
20	return protocol.KillResult{
21		RequestID: 0x1122334455,
22		Outcome:   protocol.KillFailed,
23		Code:      protocol.ErrNoSuchSession,
24		Text:      "no such session: work",
25		Failures: []protocol.KillFailure{
26			{Class: "live", Name: "one", Text: "boom"},
27			{Class: "stopped", Name: "two", Text: "gone"},
28		},
29	}
30}
31
32// TestKillResultByteRoundTrip proves one semantic KillResult re-encodes to
33// byte-identical bytes after a scan + generated unmarshal + semantic decode.
34func TestKillResultByteRoundTrip(t *testing.T) {
35	message := testKillResult()
36	raw, err := EncodeServerMessage(message)
37	require.NoError(t, err)
38	require.NoError(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, raw))
39
40	decoded, err := DecodeServerEnvelope(raw)
41	require.NoError(t, err)
42	require.Equal(t, message, decoded)
43
44	reencoded, err := EncodeServerMessage(decoded)
45	require.NoError(t, err)
46	require.Equal(t, raw, reencoded, "KillResult must re-encode byte-for-byte")
47}
48
49// TestKillRequestByteRoundTrip proves one Kill request with a unique nonzero
50// RequestID survives the client direction byte-for-byte.
51func TestKillRequestByteRoundTrip(t *testing.T) {
52	message := protocol.Kill{RequestID: 0xDEADBEEF, Name: "work", Scope: protocol.KillSession}
53	raw, err := EncodeClientMessage(message)
54	require.NoError(t, err)
55	require.NoError(t, wire.ScanEnvelope(&wire.ClientEnvelope{}, raw))
56
57	decoded, err := DecodeClientEnvelope(raw)
58	require.NoError(t, err)
59	require.Equal(t, message, decoded)
60
61	reencoded, err := EncodeClientMessage(decoded)
62	require.NoError(t, err)
63	require.Equal(t, raw, reencoded, "Kill must re-encode byte-for-byte")
64}
65
66// TestKillResultWrongDirectionFailsClosed proves a server-only KillResult
67// payload is refused on the client direction and the server decoder rejects a
68// nil envelope, so a result can never be consumed as a request.
69func TestKillResultWrongDirectionFailsClosed(t *testing.T) {
70	raw, err := proto.Marshal(&wire.ServerEnvelope{Payload: &wire.ServerEnvelope_KillResult{KillResult: killResultToWire(testKillResult())}})
71	require.NoError(t, err)
72
73	var clientEnvelope wire.ClientEnvelope
74	err = wire.ScanEnvelope(&clientEnvelope, raw)
75	require.Error(t, err, "a server KillResult must not scan as a client envelope")
76
77	_, err = decodeProtoClient(&wire.ClientEnvelope{Payload: nil})
78	require.ErrorIs(t, err, ErrWrongDirection)
79	_, err = decodeProtoServer(nil)
80	require.ErrorIs(t, err, ErrInvalidMessage)
81}
82
83// TestKillAndKillResultRejectMissingOrOutOfRangeIdentity proves the decode
84// boundary refuses a zero RequestID, an unknown outcome, an out-of-range code,
85// and a nil failure entry instead of aliasing them into valid values.
86func TestKillAndKillResultRejectMissingOrOutOfRangeIdentity(t *testing.T) {
87	tests := []struct {
88		name    string
89		run     func(t *testing.T) error
90		wantErr error
91	}{
92		{
93			name: "kill zero request id",
94			run: func(t *testing.T) error {
95				_, err := killFromWire(&wire.Kill{Name: "work", Scope: uint32(protocol.KillSession)})
96				return err
97			},
98			wantErr: errProtoConvertRange,
99		},
100		{
101			name:    "kill nil",
102			run:     func(t *testing.T) error { _, err := killFromWire(nil); return err },
103			wantErr: errProtoConvertRange,
104		},
105		{
106			name:    "kill result nil",
107			run:     func(t *testing.T) error { _, err := killResultFromWire(nil); return err },
108			wantErr: errProtoConvertRange,
109		},
110		{
111			name: "kill result zero request id",
112			run: func(t *testing.T) error {
113				_, err := killResultFromWire(&wire.KillResult{RequestId: 0, Outcome: uint32(protocol.KillSucceeded)})
114				return err
115			},
116			wantErr: errProtoConvertRange,
117		},
118		{
119			name: "kill result unknown outcome below range",
120			run: func(t *testing.T) error {
121				_, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: 0})
122				return err
123			},
124			wantErr: errProtoConvertRange,
125		},
126		{
127			name: "kill result unknown outcome above range",
128			run: func(t *testing.T) error {
129				_, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: uint32(protocol.KillOutcomeUnknown) + 1})
130				return err
131			},
132			wantErr: errProtoConvertRange,
133		},
134		{
135			name: "kill result code overflow",
136			run: func(t *testing.T) error {
137				_, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: uint32(protocol.KillFailed), Code: 1 << 16})
138				return err
139			},
140			wantErr: errProtoConvertRange,
141		},
142		{
143			name: "kill result nil failure entry",
144			run: func(t *testing.T) error {
145				_, err := killResultFromWire(&wire.KillResult{RequestId: 1, Outcome: uint32(protocol.KillFailed), Failures: []*wire.KillFailure{nil}})
146				return err
147			},
148			wantErr: errProtoConvertRange,
149		},
150	}
151	for _, tt := range tests {
152		t.Run(tt.name, func(t *testing.T) {
153			require.ErrorIs(t, tt.run(t), tt.wantErr)
154		})
155	}
156}
157
158// TestKillResultConversionArmInventory is the sessionwire arm inventory for the
159// explicit kill result: every arm of the kill encoders is exercised (server
160// result value/pointer, client request value/pointer), each arm shares the
161// value arm's bytes, and a typed nil pointer arm fails closed with
162// ErrInvalidMessage instead of panicking. A new arm added without coverage fails
163// here.
164func TestKillResultConversionArmInventory(t *testing.T) {
165	result := testKillResult()
166	resultRaw, err := EncodeServerMessage(result)
167	require.NoError(t, err)
168	request := protocol.Kill{RequestID: 0xDEADBEEF, Name: "work", Scope: protocol.KillSession}
169	requestRaw, err := EncodeClientMessage(request)
170	require.NoError(t, err)
171
172	tests := []struct {
173		name string
174		// run encodes one arm and returns the bytes and any error.
175		run     func() ([]byte, error)
176		wantRaw []byte
177		// wantErr, when non-nil, is the required failure; otherwise run must
178		// succeed with wantRaw.
179		wantErr error
180	}{
181		{name: "result value arm", run: func() ([]byte, error) { return EncodeServerMessage(result) }, wantRaw: resultRaw},
182		{name: "result pointer arm", run: func() ([]byte, error) { return EncodeServerMessage(&result) }, wantRaw: resultRaw},
183		{name: "result typed nil pointer arm", run: func() ([]byte, error) { return EncodeServerMessage((*protocol.KillResult)(nil)) }, wantErr: ErrInvalidMessage},
184		{name: "request value arm", run: func() ([]byte, error) { return EncodeClientMessage(request) }, wantRaw: requestRaw},
185		{name: "request pointer arm", run: func() ([]byte, error) { return EncodeClientMessage(&request) }, wantRaw: requestRaw},
186		{name: "request typed nil pointer arm", run: func() ([]byte, error) { return EncodeClientMessage((*protocol.Kill)(nil)) }, wantErr: ErrInvalidMessage},
187	}
188	for _, tt := range tests {
189		t.Run(tt.name, func(t *testing.T) {
190			raw, err := tt.run()
191			if tt.wantErr != nil {
192				require.ErrorIs(t, err, tt.wantErr)
193				return
194			}
195			require.NoError(t, err)
196			require.Equal(t, tt.wantRaw, raw, "every arm must share the value arm's bytes")
197		})
198	}
199
200	// The pointer arm decodes back to the same semantic value, so it is not a
201	// side channel around validation.
202	decoded, err := DecodeServerEnvelope(resultRaw)
203	require.NoError(t, err)
204	require.Equal(t, result, decoded)
205}
206
207// TestKillResultEnvelopeStrictScan proves the scan boundary rejects a
208// truncated payload, trailing bytes after a complete variant, and a
209// concatenated duplicate before any semantic decode.
210func TestKillResultEnvelopeStrictScan(t *testing.T) {
211	valid, err := EncodeServerMessage(testKillResult())
212	require.NoError(t, err)
213	require.NoError(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, valid))
214
215	require.Error(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, valid[:len(valid)-1]))
216	require.Error(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, append(append([]byte(nil), valid...), 0xFF)))
217	require.Error(t, wire.ScanEnvelope(&wire.ServerEnvelope{}, append(append([]byte(nil), valid...), valid...)))
218
219	_, err = DecodeServerEnvelope(valid[:len(valid)-1])
220	require.Error(t, err)
221	var failure *protocol.DecodeFailure
222	require.ErrorAs(t, err, &failure)
223	require.Equal(t, protocol.DecodeMalformed, failure.Category)
224}
225
226// TestKillResultIsBoundedControlEnvelope proves a KillResult rides the strict
227// control ceiling, never the bulk exemption, on both send and receive.
228func TestKillResultIsBoundedControlEnvelope(t *testing.T) {
229	message := testKillResult()
230
231	raw, err := EncodeServerMessage(message)
232	require.NoError(t, err)
233	require.Equal(t, categoryControl, serverEnvelopeCategory(raw), "a KillResult is a control envelope")
234
235	// A text sized so the serialized control envelope crosses the 1 MiB
236	// ceiling while the semantic value itself is only the declared cap.
237	oversize, err := EncodeServerMessage(protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded, Text: string(make([]byte, wire.ControlEnvelopeLimit))})
238	require.NoError(t, err)
239	require.ErrorIs(t, checkCategoryCeiling(oversize, categoryControl, wire.AbsoluteEnvelopeLimit), wire.ErrScanLength,
240		"a KillResult above the control ceiling must fail even under an absolute negotiated ceiling")
241
242	send := &serverConnection{handshake: handshake{raw: &scriptedTransport{}, ceilings: defaultProtoCeilings()}}
243	require.NoError(t, send.SendServer(message))
244	require.ErrorIs(t, send.SendServer(protocol.KillResult{RequestID: 1, Outcome: protocol.KillSucceeded, Text: string(make([]byte, wire.ControlEnvelopeLimit))}), wire.ErrScanLength)
245
246	recv := &clientConnection{handshake: handshake{raw: &scriptedTransport{recv: []wire.Envelope{{Payload: oversize}}}, ceilings: defaultProtoCeilings()}}
247	recv.preambleOnce.Do(func() {})
248	_, err = recv.ReceiveServer()
249	require.Error(t, err)
250}

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