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