v0.2.0 80c61e3

attachment_parking_test.go RawHistoryFolder

1package daemon
2
3import (
4	"errors"
5	"testing"
6
7	"github.com/stretchr/testify/require"
8
9	"github.com/bnema/vev/internal/ports"
10)
11
12type resumeWelcomeFailureTransport struct {
13	closeTrackingTransport
14}
15
16func (*resumeWelcomeFailureTransport) Send(ports.Frame) error { return errWelcomeSendFailed }
17
18var errWelcomeSendFailed = errors.New("welcome send failed")
19
20func TestAttachmentLossParksOnlyThatAttachment(t *testing.T) {
21	pty, release := newBlockingPTY(t)
22	defer release()
23	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
24
25	firstTransport := &closeTrackingTransport{}
26	sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
27	require.NoError(t, err)
28	secondTransport := &closeTrackingTransport{}
29	_, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
30	require.NoError(t, err)
31
32	d.clientGone(sess, first, firstTransport, false)
33
34	d.mu.Lock()
35	parked := d.parked[first.resumeToken]
36	d.mu.Unlock()
37	require.NotNil(t, parked)
38	sess.mu.Lock()
39	_, firstAttached := sess.attachments[first]
40	_, secondAttached := sess.attachments[second]
41	sess.mu.Unlock()
42	require.False(t, firstAttached)
43	require.True(t, secondAttached)
44	require.Contains(t, sess.snapshotAttachments(), second)
45	require.False(t, secondTransport.Closed())
46}
47
48func TestTwoAttachmentsParkIndependently(t *testing.T) {
49	pty, release := newBlockingPTY(t)
50	defer release()
51	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
52	firstTransport := &closeTrackingTransport{}
53	sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
54	require.NoError(t, err)
55	secondTransport := &closeTrackingTransport{}
56	_, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
57	require.NoError(t, err)
58	firstToken, secondToken := first.resumeToken, second.resumeToken
59
60	d.clientGone(sess, first, firstTransport, false)
61	d.clientGone(sess, second, secondTransport, false)
62	d.mu.Lock()
63	firstParked, secondParked := d.parked[firstToken], d.parked[secondToken]
64	d.mu.Unlock()
65	require.NotNil(t, firstParked)
66	require.NotNil(t, secondParked)
67	require.NotSame(t, firstParked, secondParked)
68	sess.mu.Lock()
69	require.Empty(t, sess.attachments)
70	sess.mu.Unlock()
71}
72
73func TestParkExpiryRemovesOnlyOneAttachment(t *testing.T) {
74	pty, release := newBlockingPTY(t)
75	defer release()
76	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
77	firstTransport := &closeTrackingTransport{}
78	sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
79	require.NoError(t, err)
80	secondTransport := &closeTrackingTransport{}
81	_, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
82	require.NoError(t, err)
83	d.clientGone(sess, first, firstTransport, false)
84	d.clientGone(sess, second, secondTransport, false)
85	firstToken, secondToken := first.resumeToken, second.resumeToken
86	d.mu.Lock()
87	firstParked := d.parked[firstToken]
88	d.mu.Unlock()
89	d.expireParked(firstToken, firstParked)
90	d.mu.Lock()
91	_, firstRetained := d.parked[firstToken]
92	_, secondRetained := d.parked[secondToken]
93	d.mu.Unlock()
94	require.False(t, firstRetained)
95	require.True(t, secondRetained)
96}
97
98func TestResumeRotatesCredentialAndRejectsStaleTransport(t *testing.T) {
99	pty, release := newBlockingPTY(t)
100	defer release()
101	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
102	oldTransport := &closeTrackingTransport{}
103	sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
104	require.NoError(t, err)
105	oldToken := ac.resumeToken
106	oldGeneration := ac.connectionGeneration.Load()
107	d.clientGone(sess, ac, oldTransport, false)
108
109	newTransport := &closeTrackingTransport{}
110	resumed, same, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", oldToken), newTransport, defaultSize)
111	require.NoError(t, err)
112	require.True(t, ok)
113	require.Same(t, sess, resumed)
114	require.Same(t, ac, same)
115	require.NotEqual(t, oldToken, ac.resumeToken)
116	require.Greater(t, ac.connectionGeneration.Load(), oldGeneration)
117	require.True(t, d.commitResumeClaim(ac))
118	d.mu.Lock()
119	_, oldRetained := d.parked[oldToken]
120	d.mu.Unlock()
121	require.False(t, oldRetained)
122
123	before := ac.resumeToken
124	d.clientGone(sess, ac, oldTransport, false)
125	require.Equal(t, before, ac.resumeToken)
126	d.mu.Lock()
127	_, staleParked := d.parked[oldToken]
128	d.mu.Unlock()
129	require.False(t, staleParked)
130}
131
132func TestSuccessfulResumeRejectsOldCredentialAfterWelcome(t *testing.T) {
133	pty, release := newBlockingPTY(t)
134	defer release()
135	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
136	oldTransport := &closeTrackingTransport{}
137	sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
138	require.NoError(t, err)
139	oldToken := ac.resumeToken
140	ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})
141	d.clientGone(sess, ac, oldTransport, false)
142
143	resumedTransport := &closeTrackingTransport{}
144	d.handleHello(resumedTransport, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(ports.IntentResume, "work", oldToken))})
145	newToken := ac.resumeToken
146	require.NotEqual(t, oldToken, newToken)
147	d.mu.Lock()
148	_, oldRetained := d.parked[oldToken]
149	newParked := d.parked[newToken]
150	d.mu.Unlock()
151	require.False(t, oldRetained)
152	require.NotNil(t, newParked)
153	require.NotEmpty(t, resumedTransport.Sends())
154	require.Equal(t, ports.MsgWelcome, resumedTransport.Sends()[0].Type, "Welcome must remain the first server frame after Hello")
155}
156
157func TestFailedResumeHandshakeKeepsParkedCredential(t *testing.T) {
158	pty, release := newBlockingPTY(t)
159	defer release()
160	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
161	oldTransport := &closeTrackingTransport{}
162	sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
163	require.NoError(t, err)
164	token := ac.resumeToken
165	d.clientGone(sess, ac, oldTransport, false)
166
167	failed := &resumeWelcomeFailureTransport{}
168	d.handleHello(failed, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(ports.IntentResume, "work", token))})
169	require.Nil(t, ac.transport())
170	d.mu.Lock()
171	parked := d.parked[token]
172	claimed := parked != nil && parked.claimed
173	d.mu.Unlock()
174	require.NotNil(t, parked)
175	require.False(t, claimed)
176	require.Equal(t, token, ac.resumeToken)
177
178	_, resumed, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
179	require.NoError(t, err)
180	require.True(t, ok)
181	require.Same(t, ac, resumed)
182}
183
184func TestConcurrentResumeHasExactlyOneWinner(t *testing.T) {
185	pty, release := newBlockingPTY(t)
186	defer release()
187	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
188	oldTransport := &closeTrackingTransport{}
189	sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
190	require.NoError(t, err)
191	token := ac.resumeToken
192	d.clientGone(sess, ac, oldTransport, false)
193
194	type result struct {
195		ok  bool
196		err error
197	}
198	results := make(chan result, 2)
199	for range 2 {
200		go func() {
201			_, _, ok, resumeErr := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
202			results <- result{ok: ok, err: resumeErr}
203		}()
204	}
205	var winners int
206	for range 2 {
207		got := <-results
208		if got.ok {
209			winners++
210		}
211	}
212	require.Equal(t, 1, winners)
213	require.NotEqual(t, token, ac.resumeToken)
214}
215
216func TestKillSessionClosesEveryAttachmentAndParkedCredential(t *testing.T) {
217	pty, release := newBlockingPTY(t)
218	defer release()
219	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
220	firstTransport := &closeTrackingTransport{}
221	sess, _, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
222	require.NoError(t, err)
223	secondTransport := &closeTrackingTransport{}
224	_, _, err = d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
225	require.NoError(t, err)
226	parkedTransport := &closeTrackingTransport{}
227	_, parkedAttachment, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), parkedTransport)
228	require.NoError(t, err)
229	d.clientGone(sess, parkedAttachment, parkedTransport, false)
230	parkedToken := parkedAttachment.resumeToken
231
232	require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))
233	d.waitNotifies()
234	require.True(t, firstTransport.Closed())
235	require.True(t, secondTransport.Closed())
236	require.True(t, parkedTransport.Closed())
237	d.mu.Lock()
238	_, retained := d.parked[parkedToken]
239	d.mu.Unlock()
240	require.False(t, retained)
241}
242
243func TestResumeCredentialIsInvalidAfterDaemonRestart(t *testing.T) {
244	pty, release := newBlockingPTY(t)
245	defer release()
246	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
247	transport := &closeTrackingTransport{}
248	_, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), transport)
249	require.NoError(t, err)
250	token := ac.resumeToken
251	restarted := newTestDaemon(t, newFactory(t, pty), stubClock{})
252	_, _, err = restarted.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})
253	var protocolErr *protoErr
254	require.ErrorAs(t, err, &protocolErr)
255	require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
256}
257
258func TestHandleHelloAllowsMultipleAttachments(t *testing.T) {
259	pty, release := newBlockingPTY(t)
260	defer release()
261	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
262	firstTransport := &closeTrackingTransport{}
263	sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)
264	require.NoError(t, err)
265	secondTransport := &closeTrackingTransport{}
266	_, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)
267	require.NoError(t, err)
268	require.Equal(t, true, sess.attachmentRegistered(first))
269	require.Equal(t, true, sess.attachmentRegistered(second))
270	require.NotSame(t, first, second)
271}

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