1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
|
package hotline
import (
"bytes"
"context"
"errors"
"io"
"net"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// acceptHandshakeServer starts a TCP listener that, for a single connection, plays the server side
// of the handshake (optionally sending a bad response) and then reads the login transaction the
// client sends. The received login bytes are delivered on loginCh. It returns the listener's
// address so the client can dial it.
func acceptHandshakeServer(t *testing.T, goodHandshake bool) (addr string, loginCh <-chan []byte) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
ch := make(chan []byte, 1)
go func() {
conn, err := ln.Accept()
if err != nil {
return
}
defer func() { _ = conn.Close() }()
// Read the client handshake.
hs := make([]byte, handshakeSize)
if _, err := io.ReadFull(conn, hs); err != nil {
return
}
if goodHandshake {
_, _ = conn.Write(ServerHandshake)
} else {
_, _ = conn.Write([]byte{0, 0, 0, 0, 0, 0, 0, 1})
return
}
// Read the login transaction that Connect sends next.
buf := make([]byte, 4096)
n, _ := conn.Read(buf)
ch <- buf[:n]
// Hold the connection open briefly so the client's keepalive goroutine has a live socket.
time.Sleep(50 * time.Millisecond)
}()
return ln.Addr().String(), ch
}
func TestClient_Connect(t *testing.T) {
t.Run("completes handshake and sends login", func(t *testing.T) {
addr, loginCh := acceptHandshakeServer(t, true)
c := newTestClient()
c.Pref.Username = "testuser"
require.NoError(t, c.Connect(addr, "admin", "password"))
defer func() { _ = c.Disconnect() }()
// Connect must establish the connection and arm the keepalive done channel.
assert.NotNil(t, c.Connection)
assert.NotNil(t, c.done)
// The server should have received a well-formed login transaction carrying the
// obfuscated credentials and the username.
select {
case raw := <-loginCh:
var login Transaction
_, err := login.Write(raw)
require.NoError(t, err)
assert.Equal(t, TranLogin, login.Type)
assert.Equal(t, "admin", login.GetField(FieldUserLogin).DecodeObfuscatedString())
assert.Equal(t, "password", login.GetField(FieldUserPassword).DecodeObfuscatedString())
assert.Equal(t, []byte("testuser"), login.GetField(FieldUserName).Data)
case <-time.After(2 * time.Second):
t.Fatal("server did not receive login transaction")
}
})
t.Run("returns error when dial fails", func(t *testing.T) {
// Reserve a port, then close the listener so nothing is accepting on it.
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := ln.Addr().String()
require.NoError(t, ln.Close())
c := newTestClient()
err = c.Connect(addr, "admin", "password")
assert.Error(t, err)
})
t.Run("returns error on bad handshake response", func(t *testing.T) {
addr, _ := acceptHandshakeServer(t, false)
c := newTestClient()
err := c.Connect(addr, "admin", "password")
assert.Error(t, err)
assert.Contains(t, err.Error(), "handshake")
})
}
func TestClient_HandleTransactions(t *testing.T) {
t.Run("dispatches queued transactions then reports termination", func(t *testing.T) {
// Two server-initiated transactions back to back, followed by EOF.
var buf bytes.Buffer
buf.Write(serializeTransaction(t, NewTransaction(TranChatMsg, [2]byte{0, 0}, NewField(FieldData, []byte("hi")))))
buf.Write(serializeTransaction(t, NewTransaction(TranChatMsg, [2]byte{0, 0}, NewField(FieldData, []byte("there")))))
c := newTestClient()
c.Connection = &clientMockConn{RBuf: &buf, WBuf: &bytes.Buffer{}}
var got []string
c.HandleFunc(TranChatMsg, func(_ context.Context, _ *Client, tr *Transaction) ([]Transaction, error) {
got = append(got, string(tr.GetField(FieldData).Data))
return nil, nil
})
err := c.HandleTransactions(context.Background())
// At EOF the scanner stops and HandleTransactions reports the connection is gone.
assert.EqualError(t, err, "connection terminated")
assert.Equal(t, []string{"hi", "there"}, got)
})
t.Run("returns scanner error", func(t *testing.T) {
wantErr := errors.New("read boom")
c := newTestClient()
c.Connection = &errorConn{err: wantErr}
err := c.HandleTransactions(context.Background())
assert.ErrorIs(t, err, wantErr)
})
}
func TestClient_keepalive_stopsOnDone(t *testing.T) {
c := newTestClient()
done := make(chan struct{})
errCh := make(chan error, 1)
go func() { errCh <- c.keepalive(done) }()
// Closing done must cause keepalive to return promptly (well before the 300s tick).
close(done)
select {
case err := <-errCh:
assert.NoError(t, err)
case <-time.After(1 * time.Second):
t.Fatal("keepalive did not return after done was closed")
}
}
// errorConn is a net.Conn whose Read always fails with a non-EOF error, so a bufio.Scanner over it
// surfaces the error via Scanner.Err().
type errorConn struct {
err error
}
func (e *errorConn) Read([]byte) (int, error) { return 0, e.err }
func (e *errorConn) Write(b []byte) (int, error) { return len(b), nil }
func (e *errorConn) Close() error { return nil }
func (e *errorConn) LocalAddr() net.Addr { return nil }
func (e *errorConn) RemoteAddr() net.Addr { return nil }
func (e *errorConn) SetDeadline(time.Time) error { return nil }
func (e *errorConn) SetReadDeadline(time.Time) error { return nil }
func (e *errorConn) SetWriteDeadline(time.Time) error { return nil }
|