v0.3.0 99dc266

wire_final_test.go RawHistoryFolder

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 != 31 {
15		t.Fatalf("ProtocolVersion = %d, want 31", 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, 31, 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, 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	local := AttachTarget{Session: "work", Intent: IntentAttach}
193	localPayload := MarshalAttachTarget(local)
194	if localPayload == nil {
195		t.Fatal("MarshalAttachTarget rejected same-peer route handoff")
196	}
197	wantLocal := []byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 0}
198	if !bytes.Equal(localPayload, wantLocal) {
199		t.Fatalf("same-peer bytes = %x, want %x", localPayload, wantLocal)
200	}
201	decodedLocal, err := UnmarshalAttachTarget(localPayload)
202	if err != nil || decodedLocal != local {
203		t.Fatalf("same-peer target = %+v, error %v, want %+v", decodedLocal, err, local)
204	}
205	assertAllPrefixesFail(t, localPayload, UnmarshalAttachTarget)
206	assertTrailingGarbageFails(t, localPayload, UnmarshalAttachTarget)
207	for _, bad := range []AttachTarget{{Endpoint: "host", Intent: IntentAttach}, {Endpoint: "host", Session: "work", Intent: 99}} {
208		if got := MarshalAttachTarget(bad); got != nil {
209			t.Fatalf("MarshalAttachTarget(%+v) = %x, want nil", bad, got)
210		}
211	}
212}
213
214func TestAttachTargetExactTargetWireStrict(t *testing.T) {
215	var lifecycle domain.SessionLifecycleID
216	lifecycle[0] = 1
217	target := AttachTarget{Session: "work", Intent: IntentAttach, ExactTarget: &ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}}
218	payload := MarshalAttachTarget(target)
219	want := append([]byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 1, 1}, make([]byte, 15)...)
220	want = append(want, 0, 4, 'w', 'o', 'r', 'k')
221	if !bytes.Equal(payload, want) {
222		t.Fatalf("exact target bytes = %x, want %x", payload, want)
223	}
224	back, err := UnmarshalAttachTarget(payload)
225	if err != nil || !reflect.DeepEqual(back, target) {
226		t.Fatalf("exact target = %+v, error %v, want %+v", back, err, target)
227	}
228	assertAllPrefixesFail(t, payload, UnmarshalAttachTarget)
229	assertTrailingGarbageFails(t, payload, UnmarshalAttachTarget)
230	mismatched := target
231	mismatched.ExactTarget = &ExactSessionTarget{LifecycleID: lifecycle, SessionName: "other"}
232	if got := MarshalAttachTarget(mismatched); got != nil {
233		t.Fatalf("MarshalAttachTarget accepted mismatched target: %x", got)
234	}
235}
236
237func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
238	tests := []struct {
239		name    string
240		payload []byte
241		decode  func([]byte) error
242	}{
243		{
244			name: "hello boolean",
245			payload: func() []byte {
246				b := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})
247				b[len(b)-8] = 2
248				return b
249			}(),
250			decode: func(b []byte) error { _, err := UnmarshalHello(b); return err },
251		},
252		{
253			name: "welcome boolean",
254			payload: func() []byte {
255				b := MarshalWelcome(Welcome{SessionID: "id"})
256				b[6] = 2
257				return b
258			}(),
259			decode: func(b []byte) error { _, err := UnmarshalWelcome(b); return err },
260		},
261		{name: "detached reason", payload: []byte{0xff}, decode: func(b []byte) error { _, err := UnmarshalDetached(b); return err }},
262		{
263			name: "kill boolean",
264			payload: func() []byte {
265				b := MarshalKill(Kill{All: true})
266				b[len(b)-1] = 2
267				return b
268			}(),
269			decode: func(b []byte) error { _, err := UnmarshalKill(b); return err },
270		},
271		{
272			name: "sessions ephemeral boolean",
273			payload: func() []byte {
274				b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
275				b[6] = 2
276				return b
277			}(),
278			decode: func(b []byte) error { _, err := UnmarshalSessions(b); return err },
279		},
280		{
281			name: "sessions attached boolean",
282			payload: func() []byte {
283				b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})
284				b[9] = 2
285				return b
286			}(),
287			decode: func(b []byte) error { _, err := UnmarshalSessions(b); return err },
288		},
289		{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 }},
290	}
291	for _, tt := range tests {
292		t.Run(tt.name, func(t *testing.T) {
293			if err := tt.decode(tt.payload); err == nil {
294				t.Fatal("decoder accepted unknown closed wire value")
295			}
296		})
297	}
298}

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