client_handshake_test.go RawHistoryFolder
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// TestClientHandshakeCompletionSeam proves the handshake deadline is stable for
14// the connection's lifetime and completion is published exactly once to early
15// and late subscribers.
16func TestClientHandshakeCompletionSeam(t *testing.T) {
17 raw := &scriptedTransport{recv: []wire.Envelope{mustPreambleResponse(t)}}
18 conn := NewClientConnection(raw).(*clientConnection)
19
20 deadline := conn.HandshakeDeadline()
21 require.WithinDuration(t, time.Now().Add(protocol.HandshakeTimeout), deadline, 5*time.Second)
22 require.Equal(t, deadline, conn.HandshakeDeadline(), "the resolved deadline is stable")
23
24 select {
25 case <-conn.HandshakeDone():
26 t.Fatal("HandshakeDone closed before the handshake ran")
27 default:
28 }
29
30 early := make(chan struct{}, 1)
31 conn.OnHandshakeComplete(func() { early <- struct{}{} })
32 select {
33 case <-early:
34 t.Fatal("completion ran before the handshake")
35 default:
36 }
37
38 // The first application send runs the lazy preamble, which publishes
39 // completion.
40 require.NoError(t, conn.SendClient(protocol.Ping{}))
41 select {
42 case <-early:
43 case <-time.After(5 * time.Second):
44 t.Fatal("registered completion hook did not run")
45 }
46 select {
47 case <-conn.HandshakeDone():
48 case <-time.After(5 * time.Second):
49 t.Fatal("HandshakeDone did not close")
50 }
51
52 // A hook registered after completion runs immediately.
53 late := make(chan struct{}, 1)
54 conn.OnHandshakeComplete(func() { late <- struct{}{} })
55 select {
56 case <-late:
57 case <-time.After(5 * time.Second):
58 t.Fatal("late completion hook did not run")
59 }
60}
61
62// TestClientFinishHandshakeNilSafe proves finishPreamble and runHandshakeHooks
63// tolerate a connection literal that never allocated its done channel, and that
64// the hook bookkeeping still completes.
65func TestClientFinishHandshakeNilSafe(t *testing.T) {
66 conn := &clientConnection{}
67 require.NotPanics(t, func() {
68 conn.finishPreamble()
69 conn.runHandshakeHooks()
70 })
71
72 ran := false
73 conn.OnHandshakeComplete(func() { ran = true })
74 require.True(t, ran, "a hook registered after completion runs immediately")
75}
76
77// TestClientHandshakeHookReentersConnection proves a completion hook that calls
78// back into the connection does not re-enter the preamble's sync.Once and
79// deadlock: the hook runs after the Once body has returned.
80func TestClientHandshakeHookReentersConnection(t *testing.T) {
81 raw := &scriptedTransport{recv: mustClientPreambleQueue(t, mustEncodeServer(t, protocol.Pong{}))}
82 conn := NewClientConnection(raw).(*clientConnection)
83
84 sent := make(chan error, 1)
85 conn.OnHandshakeComplete(func() {
86 // A hook that calls a connection method must not re-enter the preamble's
87 // sync.Once and block the handshake forever.
88 sent <- conn.SendClient(protocol.Ping{})
89 })
90
91 require.NoError(t, conn.SendClient(protocol.Ping{}))
92 select {
93 case err := <-sent:
94 require.NoError(t, err)
95 case <-time.After(5 * time.Second):
96 t.Fatal("handshake hook re-entered the connection and deadlocked")
97 }
98}