1package sessionwire
2
3import (
4 "testing"
5 "time"
6
7 "github.com/stretchr/testify/require"
8
9 "github.com/bnema/vev/internal/protocol"
10 "github.com/bnema/vev/internal/protocol/wire"
11)
12
13// TestServerHandshakeCompletionSeam proves the accepted absolute deadline is
14// stable for the connection's lifetime and completion is published exactly once
15// to early and late subscribers, without restarting a deadline the caller
16// already fixed.
17func TestServerHandshakeCompletionSeam(t *testing.T) {
18 fixed := time.Now().Add(protocol.HandshakeTimeout)
19 raw := &scriptedTransport{recv: mustServerPreambleQueue(t, mustEncodeClient(t, protocol.Ping{}))}
20 conn := NewServerConnectionWithDeadline(raw, fixed).(*serverConnection)
21
22 require.Equal(t, fixed, conn.HandshakeDeadline(), "the accepted deadline is adopted verbatim")
23 require.Equal(t, fixed, conn.HandshakeDeadline(), "the resolved deadline is stable")
24
25 select {
26 case <-conn.HandshakeDone():
27 t.Fatal("HandshakeDone closed before the handshake ran")
28 default:
29 }
30
31 early := make(chan struct{}, 1)
32 conn.OnHandshakeComplete(func() { early <- struct{}{} })
33 select {
34 case <-early:
35 t.Fatal("completion ran before the handshake")
36 default:
37 }
38
39 // The first receive runs the lazy preamble, which publishes completion.
40 message, err := conn.ReceiveClient()
41 require.NoError(t, err)
42 require.Equal(t, protocol.Ping{}, message)
43
44 select {
45 case <-early:
46 case <-time.After(5 * time.Second):
47 t.Fatal("registered completion hook did not run")
48 }
49 select {
50 case <-conn.HandshakeDone():
51 case <-time.After(5 * time.Second):
52 t.Fatal("HandshakeDone did not close")
53 }
54
55 // A hook registered after completion runs immediately.
56 late := make(chan struct{}, 1)
57 conn.OnHandshakeComplete(func() { late <- struct{}{} })
58 select {
59 case <-late:
60 case <-time.After(5 * time.Second):
61 t.Fatal("late completion hook did not run")
62 }
63}
64
65// TestServerHandshakeElapsedDeadlineFailsPreamble proves a deadline the caller
66// already spent fails the lazy preamble immediately instead of starting a
67// second 15 s budget, and that completion is still published for the failed
68// handshake.
69func TestServerHandshakeElapsedDeadlineFailsPreamble(t *testing.T) {
70 raw := &scriptedTransport{recv: mustServerPreambleQueue(t, mustEncodeClient(t, protocol.Ping{}))}
71 conn := NewServerConnectionWithDeadline(raw, time.Now().Add(-time.Second)).(*serverConnection)
72
73 completed := make(chan struct{}, 1)
74 conn.OnHandshakeComplete(func() { completed <- struct{}{} })
75
76 _, err := conn.ReceiveClient()
77 require.Error(t, err)
78 require.ErrorIs(t, err, ErrPreambleTimeout)
79 select {
80 case <-completed:
81 case <-time.After(5 * time.Second):
82 t.Fatal("a failed handshake did not publish completion")
83 }
84}
85
86// TestServerFinishHandshakeNilSafe proves finishPreamble and runHandshakeHooks
87// tolerate a connection literal that never allocated its done channel, and that
88// the hook bookkeeping still completes.
89func TestServerFinishHandshakeNilSafe(t *testing.T) {
90 conn := &serverConnection{}
91 require.NotPanics(t, func() {
92 conn.finishPreamble()
93 conn.runHandshakeHooks()
94 })
95
96 ran := false
97 conn.OnHandshakeComplete(func() { ran = true })
98 require.True(t, ran, "a hook registered after completion runs immediately")
99}
100
101// TestServerHandshakeHookReentersConnection proves a completion hook that calls
102// back into the connection does not re-enter the preamble's sync.Once and
103// deadlock: the hook runs after the Once body has returned, so it can receive a
104// message of its own before the outer receive continues.
105func TestServerHandshakeHookReentersConnection(t *testing.T) {
106 // A preamble followed by two messages: one for the re-entrant hook and one
107 // for the outer receive.
108 raw := &scriptedTransport{recv: []wire.Envelope{
109 mustPreambleRequest(t),
110 {Payload: mustEncodeClient(t, protocol.Ping{})},
111 {Payload: mustEncodeClient(t, protocol.Ping{})},
112 }}
113 conn := NewServerConnection(raw).(*serverConnection)
114
115 hookErr := make(chan error, 1)
116 hookMessage := make(chan protocol.ClientMessage, 1)
117 conn.OnHandshakeComplete(func() {
118 // A hook that calls a connection method must not re-enter the preamble's
119 // sync.Once and block the handshake forever.
120 message, err := conn.ReceiveClient()
121 if err != nil {
122 hookErr <- err
123 return
124 }
125 hookMessage <- message
126 })
127
128 message, err := conn.ReceiveClient()
129 require.NoError(t, err)
130 require.Equal(t, protocol.Ping{}, message)
131 select {
132 case err := <-hookErr:
133 require.NoError(t, err)
134 case got := <-hookMessage:
135 require.Equal(t, protocol.Ping{}, got)
136 case <-time.After(5 * time.Second):
137 t.Fatal("handshake hook re-entered the connection and deadlocked")
138 }
139}