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}