v0.9.0 e5e4ded

attachment_test.go RawHistoryFolder

1package sessionwire
2
3import (
4	"encoding/hex"
5	"reflect"
6	"testing"
7
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	"google.golang.org/protobuf/proto"
13	"google.golang.org/protobuf/reflect/protoreflect"
14)
15
16func TestAttachmentEnvelopes(t *testing.T) {
17	target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
18	for _, tt := range []struct {
19		name    string
20		message any
21		golden  string
22	}{
23		{"suspend", protocol.SuspendAttachment{RequestID: 1}, "ea01020801"},
24		{"suspended", protocol.AttachmentSuspended{RequestID: 1, Target: target}, "fa011e0801121a0a120a10010000000000000000000000000000001204776f726b"},
25		{"activate", protocol.ActivateAttachment{RequestID: 2, Target: target, Size: domain.Size{Cols: 80, Rows: 24}}, "f201220802121a0a120a10010000000000000000000000000000001204776f726b18502018"},
26		{"activated", protocol.AttachmentActivated{RequestID: 2, Identity: protocol.CommittedRouteIdentity{Target: target}, Epoch: 2, State: 1, ViewPublication: 3}, "8202260802121c0a1a0a120a10010000000000000000000000000000001204776f726b180220012803"},
27	} {
28		t.Run(tt.name, func(t *testing.T) {
29			var raw []byte
30			var err error
31			var decode func([]byte) (any, error)
32			var wrong func([]byte) (any, error)
33			if m, ok := tt.message.(protocol.ClientMessage); ok {
34				raw, err = EncodeClientMessage(m)
35				decode = func(b []byte) (any, error) { return DecodeClientEnvelope(b) }
36				wrong = func(b []byte) (any, error) { return DecodeServerEnvelope(b) }
37			} else {
38				raw, err = EncodeServerMessage(tt.message.(protocol.ServerMessage))
39				decode = func(b []byte) (any, error) { return DecodeServerEnvelope(b) }
40				wrong = func(b []byte) (any, error) { return DecodeClientEnvelope(b) }
41			}
42			require.NoError(t, err)
43			require.Equal(t, tt.golden, hex.EncodeToString(raw))
44			got, err := decode(raw)
45			require.NoError(t, err)
46			require.Equal(t, tt.message, got)
47			_, err = wrong(raw)
48			require.Error(t, err)
49			for i := 0; i < len(raw); i++ {
50				_, err = decode(raw[:i])
51				require.Error(t, err, "prefix %d", i)
52			}
53			for _, tail := range [][]byte{{0}, {0x80}, raw, {0xf8, 0x07, 0x01}} {
54				_, err = decode(append(append([]byte(nil), raw...), tail...))
55				require.Error(t, err)
56			}
57		})
58	}
59}
60
61// Clearing any required field must fail on both conversion directions. Direct
62// protobuf marshal deliberately bypasses the semantic encoder for hostile input.
63func TestAttachmentRequiredFields(t *testing.T) {
64	target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
65	for _, message := range []any{
66		protocol.SuspendAttachment{RequestID: 1},
67		protocol.AttachmentSuspended{RequestID: 1, Target: target},
68		protocol.ActivateAttachment{RequestID: 2, Target: target, Size: domain.Size{Cols: 80, Rows: 24}},
69		protocol.AttachmentActivated{RequestID: 2, Identity: protocol.CommittedRouteIdentity{Target: target}, Epoch: 2, State: 1, ViewPublication: 3},
70	} {
71		t.Run(reflect.TypeOf(message).Name(), func(t *testing.T) {
72			value := reflect.ValueOf(message)
73			// Pointer forms share the same validated path; typed nil pointers fail.
74			pointer := reflect.New(value.Type())
75			pointer.Elem().Set(value)
76			nilPointer := reflect.Zero(pointer.Type()).Interface()
77			encode := func(v any) ([]byte, error) {
78				if m, ok := v.(protocol.ClientMessage); ok {
79					return EncodeClientMessage(m)
80				}
81				return EncodeServerMessage(v.(protocol.ServerMessage))
82			}
83			_, err := encode(pointer.Interface())
84			require.NoError(t, err)
85			_, err = encode(nilPointer)
86			require.Error(t, err)
87			for i := 0; i < value.NumField(); i++ {
88				if value.Type().Field(i).Name == "PixelWidth" || value.Type().Field(i).Name == "PixelHeight" {
89					continue
90				}
91				bad := reflect.New(value.Type()).Elem()
92				bad.Set(value)
93				bad.Field(i).SetZero()
94				_, err = encode(bad.Interface())
95				require.Error(t, err, value.Type().Field(i).Name)
96			}
97			var envelope proto.Message
98			var decode func([]byte) (any, error)
99			if m, ok := message.(protocol.ClientMessage); ok {
100				envelope, err = encodeProtoClient(m)
101				decode = func(b []byte) (any, error) { return DecodeClientEnvelope(b) }
102			} else {
103				envelope, err = encodeProtoServer(message.(protocol.ServerMessage))
104				decode = func(b []byte) (any, error) { return DecodeServerEnvelope(b) }
105			}
106			require.NoError(t, err)
107			descriptor := envelope.ProtoReflect().Descriptor().Oneofs().Get(0)
108			variant := envelope.ProtoReflect().WhichOneof(descriptor)
109			payload := envelope.ProtoReflect().Get(variant).Message()
110			payload.Range(func(field protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
111				clone := proto.Clone(envelope)
112				clone.ProtoReflect().Get(variant).Message().Clear(field)
113				raw, err := proto.Marshal(clone)
114				require.NoError(t, err)
115				_, err = decode(raw)
116				require.Error(t, err, string(field.Name()))
117				return true
118			})
119		})
120	}
121}
122
123func TestAttachmentActivationRejectsMalformedGeometryAndIdentity(t *testing.T) {
124	target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
125	for _, mutate := range []func(*wire.ActivateAttachment){
126		func(m *wire.ActivateAttachment) { m.Cols = 1 << 16 },
127		func(m *wire.ActivateAttachment) { m.Rows = 1 << 16 },
128		func(m *wire.ActivateAttachment) { m.PixelWidth = 1 },
129		func(m *wire.ActivateAttachment) { m.PixelWidth = 1 << 16; m.PixelHeight = 1 },
130		func(m *wire.ActivateAttachment) { m.Target.LifecycleId.Value = []byte{1} },
131		func(m *wire.ActivateAttachment) { m.Target.SessionName = "" },
132	} {
133		m := &wire.ActivateAttachment{RequestId: 2, Target: exactTargetToWire(&target), Cols: 80, Rows: 24}
134		mutate(m)
135		raw, err := proto.Marshal(&wire.ClientEnvelope{Payload: &wire.ClientEnvelope_ActivateAttachment{ActivateAttachment: m}})
136		require.NoError(t, err)
137		_, err = DecodeClientEnvelope(raw)
138		require.Error(t, err)
139	}
140}
141
142// This is a typed connection contract test, not a daemon lifecycle test. The
143// scripted peer supplies the barrier and full-publication ordering Phase 2
144// must enforce in the real daemon.
145func TestAttachmentTypedConnectionSequence(t *testing.T) {
146	context := testUIOutputContext()
147	target := context.Route.Target
148	suspend := protocol.SuspendAttachment{RequestID: 10}
149	suspended := protocol.AttachmentSuspended{RequestID: 10, Target: target}
150	activate := protocol.ActivateAttachment{RequestID: 11, Target: target, Size: domain.Size{Cols: 80, Rows: 24}, PixelWidth: 800, PixelHeight: 480}
151	full := protocol.Output{Epoch: 2, New: 1, Full: true, Size: activate.Size, Context: context, Data: []byte("full")}
152	activated := protocol.AttachmentActivated{RequestID: 11, Identity: context.Route, Epoch: full.Epoch, State: full.New, ViewPublication: context.Publication}
153	clientRaw := &scriptedTransport{recv: []wire.Envelope{mustPreambleResponse(t)}}
154	serverRaw := &scriptedTransport{recv: []wire.Envelope{mustPreambleRequest(t)}}
155	client := NewClientConnection(clientRaw)
156	server := NewServerConnection(serverRaw)
157	t.Cleanup(func() { require.NoError(t, client.Close()); require.NoError(t, server.Close()) })
158	sendClient := func(message protocol.ClientMessage) {
159		require.NoError(t, client.SendClient(message))
160		serverRaw.recv = append(serverRaw.recv, wire.Envelope{Payload: clientRaw.sentPayload(clientRaw.sentLen() - 1)})
161		got, err := server.ReceiveClient()
162		require.NoError(t, err)
163		require.Equal(t, message, got)
164	}
165	sendServer := func(message protocol.ServerMessage) {
166		require.NoError(t, server.SendServer(message))
167		clientRaw.recv = append(clientRaw.recv, wire.Envelope{Payload: serverRaw.sentPayload(serverRaw.sentLen() - 1)})
168		got, err := client.ReceiveServer()
169		require.NoError(t, err)
170		require.Equal(t, message, got)
171	}
172	sendClient(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: target.SessionName, ExactTarget: &target, Size: activate.Size})
173	sendServer(protocol.Welcome{SessionID: "session", SessionName: target.SessionName, CommittedIdentity: &context.Route})
174	initial := full
175	initial.Epoch = 1
176	sendServer(initial)
177	sendClient(suspend)
178	sendServer(suspended)
179	sendClient(protocol.Ping{})
180	sendServer(protocol.Pong{})
181	sendClient(activate)
182	sendServer(full)
183	sendServer(activated)
184	require.NotEqual(t, initial.Epoch, activated.Epoch)
185	require.Equal(t, activate.RequestID, activated.RequestID)
186	require.Equal(t, activate.Target, activated.Identity.Target)
187	sendClient(protocol.Input{InputSeq: 1, Data: []byte("x")})
188}

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