v0.2.0 80c61e3

remote_preview_hardening_test.go RawHistoryFolder

1package ports
2
3import (
4	"bytes"
5	"math"
6	"strings"
7	"testing"
8
9	renderer "github.com/bnema/vev-vt"
10	"github.com/bnema/vev/internal/domain"
11)
12
13const (
14	remotePreviewGoldenStatusOffset         = 2
15	remotePreviewGoldenCellsOffset          = 2 + 1 + 16 + 2 + len("tab-1") + 8 + 2 + 2 + 4
16	remotePreviewGoldenCellFlagsOffset      = remotePreviewGoldenCellsOffset + 4
17	remotePreviewGoldenStyleFlagsOffset     = remotePreviewGoldenCellsOffset + 5
18	remotePreviewGoldenStyleAttrsOffset     = remotePreviewGoldenCellsOffset + 6
19	remotePreviewGoldenSecondCellOffset     = remotePreviewGoldenCellsOffset + previewCellWireSize
20	remotePreviewGoldenUnderlineStyleOffset = remotePreviewGoldenSecondCellOffset + 12
21)
22
23func remotePreviewGoldenPayload() []byte {
24	return []byte{
25		0x00, 0x01, 0x00,
26		0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
27		0x00, 0x05, 't', 'a', 'b', '-', '1',
28		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
29		0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02,
30		0x00, 0x00, 0x00, 0x6f, 0x00,
31		0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff,
32		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
33		0x00, 0x00, 0x00, 0x6b, 0x00,
34		0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff,
35		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
36	}
37}
38
39func TestRemotePreviewCodecGoldenRoundTrip(t *testing.T) {
40	golden := remotePreviewGoldenPayload()
41	got, err := UnmarshalRemotePreview(golden)
42	if err != nil {
43		t.Fatalf("UnmarshalRemotePreview() error = %v", err)
44	}
45	if got.Version != RemotePreviewSchemaVersion || got.Status != RemotePreviewOK {
46		t.Fatalf("header = (%d, %d), want (%d, %d)", got.Version, got.Status, RemotePreviewSchemaVersion, RemotePreviewOK)
47	}
48	var wantLifecycle domain.SessionLifecycleID
49	for i := range wantLifecycle {
50		wantLifecycle[i] = byte(i + 1)
51	}
52	if got.LifecycleID != wantLifecycle || got.TabID != "tab-1" || got.Revision != 1 || got.Width != 2 || got.Height != 1 {
53		t.Fatalf("metadata = %#v, want lifecycle/tab/revision/size from golden", got)
54	}
55	if len(got.Cells) != 2 || got.Cells[0].Rune != 'o' || got.Cells[1].Rune != 'k' {
56		t.Fatalf("cells = %#v, want [o k]", got.Cells)
57	}
58	if !bytes.Equal(MarshalRemotePreview(got), golden) {
59		t.Fatalf("marshal changed golden payload: %x", MarshalRemotePreview(got))
60	}
61}
62
63func TestRemotePreviewCodecRejectsInvalidStatusFlagsAndRanges(t *testing.T) {
64	golden := remotePreviewGoldenPayload()
65	tests := []struct {
66		name   string
67		mutate func([]byte)
68	}{
69		{name: "status", mutate: func(b []byte) { b[remotePreviewGoldenStatusOffset] = 0xff }},
70		{name: "cell flags", mutate: func(b []byte) { b[remotePreviewGoldenCellFlagsOffset] = 0x02 }},
71		{name: "style flags", mutate: func(b []byte) { b[remotePreviewGoldenStyleFlagsOffset] = 0x80 }},
72		{name: "style attrs", mutate: func(b []byte) { b[remotePreviewGoldenStyleAttrsOffset] = 0x80 }},
73		{name: "underline range", mutate: func(b []byte) { b[remotePreviewGoldenUnderlineStyleOffset] = byte(renderer.UnderlineDashed + 1) }},
74	}
75	for _, test := range tests {
76		t.Run(test.name, func(t *testing.T) {
77			payload := append([]byte(nil), golden...)
78			test.mutate(payload)
79			if _, err := UnmarshalRemotePreview(payload); err == nil {
80				t.Fatal("malformed payload decoded successfully")
81			}
82		})
83	}
84}
85
86func TestRemotePreviewCodecRejectsWideCellBoundaryAndInvalidIDs(t *testing.T) {
87	var lifecycle domain.SessionLifecycleID
88	lifecycle[0] = 1
89	valid := RemotePreview{
90		Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,
91		LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1,
92		Cells: []renderer.Cell{{Rune: '界'}, {Continuation: true}},
93	}
94	if MarshalRemotePreview(valid) == nil {
95		t.Fatal("valid wide-cell preview did not marshal")
96	}
97	for _, malformed := range []RemotePreview{
98		{Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: '界'}}},
99		{Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Continuation: true}, {Rune: 'x'}}},
100		{Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Rune: '界'}, {Continuation: true, Rune: 'x'}}},
101		{Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 2, Cells: []renderer.Cell{{Rune: 'x'}, {Rune: '界'}, {Continuation: true}, {Rune: 'y'}}},
102	} {
103		if MarshalRemotePreview(malformed) != nil {
104			t.Fatalf("malformed wide-cell preview marshaled: %#v", malformed.Cells)
105		}
106	}
107	invalidLifecycle := valid
108	invalidLifecycle.LifecycleID = domain.SessionLifecycleID{}
109	if MarshalRemotePreview(invalidLifecycle) != nil {
110		t.Fatal("zero lifecycle ID marshaled")
111	}
112	invalidTab := valid
113	invalidTab.TabID = "tab id"
114	if MarshalRemotePreview(invalidTab) != nil {
115		t.Fatal("invalid tab ID marshaled")
116	}
117}
118
119func TestRemotePreviewRejectsControlRunes(t *testing.T) {
120	valid := RemotePreview{
121		Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,
122		LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 1, Width: 1, Height: 1,
123		Cells: []renderer.Cell{{Rune: 'x'}},
124	}
125	for _, control := range []rune{'\x01', '\x1b', '\x7f', '\u0085'} {
126		preview := valid
127		preview.Cells = []renderer.Cell{{Rune: control}}
128		if err := ValidateRemotePreview(preview); err != ErrInvalidRemotePreview {
129			t.Fatalf("control rune %U: ValidateRemotePreview() error = %v, want %v", control, err, ErrInvalidRemotePreview)
130		}
131		if MarshalRemotePreview(preview) != nil {
132			t.Fatalf("control rune %U was marshaled", control)
133		}
134	}
135}
136
137func TestRemotePreviewRequestRejectsOversizedNestedRouteData(t *testing.T) {
138	target := previewTargetForTest()
139	target.Endpoint = strings.Repeat("h", math.MaxUint16+1)
140	request := RemotePreviewRequest{Version: RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}
141	if MarshalRemotePreviewRequest(request) != nil {
142		t.Fatal("oversized endpoint was truncated and marshaled")
143	}
144}
145
146func TestRemoteTargetWireRejectsInvalidSelectorCombinations(t *testing.T) {
147	cases := []struct {
148		name   string
149		mutate func(*domain.RemoteSessionTarget)
150	}{
151		{name: "zero lifecycle", mutate: func(target *domain.RemoteSessionTarget) { target.LifecycleID = domain.SessionLifecycleID{} }},
152		{name: "live and stopped selector", mutate: func(target *domain.RemoteSessionTarget) {
153			target.StoppedTab = domain.NewOrdinalTabSelector(0, "main", 1)
154		}},
155		{name: "stopped with live ID", mutate: func(target *domain.RemoteSessionTarget) {
156			target.Stopped = true
157			target.LiveTabID = "tab-1"
158			target.StoppedTab = domain.NewOrdinalTabSelector(0, "main", 1)
159		}},
160		{name: "stable selector with ordinal fields", mutate: func(target *domain.RemoteSessionTarget) {
161			target.Stopped = true
162			target.LiveTabID = ""
163			target.StoppedTab = domain.TabSelector{Kind: domain.TabSelectorByStableID, StableID: "tab-1", Ordinal: 1}
164		}},
165		{name: "ordinal selector with zero count", mutate: func(target *domain.RemoteSessionTarget) {
166			target.Stopped = true
167			target.LiveTabID = ""
168			target.StoppedTab = domain.NewOrdinalTabSelector(0, "main", 0)
169		}},
170		{name: "oversized tab name", mutate: func(target *domain.RemoteSessionTarget) {
171			target.Stopped = true
172			target.LiveTabID = ""
173			target.StoppedTab = domain.NewOrdinalTabSelector(0, strings.Repeat("n", 257), 1)
174		}},
175	}
176	for _, test := range cases {
177		t.Run(test.name, func(t *testing.T) {
178			target := *testRemoteTarget(false)
179			test.mutate(&target)
180			message := AttachTarget{Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach, RemoteTarget: &target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}
181			if MarshalAttachTarget(message) != nil {
182				t.Fatal("invalid target was marshaled")
183			}
184		})
185	}
186}
187
188func TestRemoteTargetWireRichGoldenRoundTrip(t *testing.T) {
189	target := testRemoteTarget(false)
190	got := MarshalAttachTarget(AttachTarget{
191		Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,
192		RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned,
193	})
194	want := []byte{
195		0x00, 0x0f, 'b', 'u', 'i', 'l', 'd', '@', 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',
196		0x00, 0x04, 'w', 'o', 'r', 'k', IntentAttach,
197		0x01,
198		0x00, 0x0f, 'b', 'u', 'i', 'l', 'd', '@', 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',
199		0x00, 0x09, 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',
200		0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
201		0x00, 0x04, 'w', 'o', 'r', 'k', 0x00,
202		0x00, 0x05, 't', 'a', 'b', '-', '3', 0x00, 0x01,
203	}
204	if !bytes.Equal(got, want) {
205		t.Fatalf("rich target bytes = %x, want %x", got, want)
206	}
207	decoded, err := UnmarshalAttachTarget(got)
208	if err != nil {
209		t.Fatalf("UnmarshalAttachTarget() error = %v", err)
210	}
211	if decoded.RemoteTarget == nil || *decoded.RemoteTarget != *target || decoded.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
212		t.Fatalf("decoded target = %#v, policy = %d", decoded.RemoteTarget, decoded.EnvironmentPolicy)
213	}
214}

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