aboutsummaryrefslogtreecommitdiff
path: root/hotline/server_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'hotline/server_test.go')
-rw-r--r--hotline/server_test.go52
1 files changed, 52 insertions, 0 deletions
diff --git a/hotline/server_test.go b/hotline/server_test.go
index a9842c8..fb9a9c7 100644
--- a/hotline/server_test.go
+++ b/hotline/server_test.go
@@ -1018,6 +1018,58 @@ func TestServer_ListenAndServe_returnsErrorWhenPortUnavailable(t *testing.T) {
assert.NotErrorIs(t, err, context.Canceled)
}
+// TestServer_ListenAndServe_closesActiveConnsOnCancel verifies that shutdown force-closes
+// accepted connections rather than only closing the listeners: a client blocked mid-handshake and
+// a file transfer connection that never sends its header would otherwise keep their session
+// goroutines alive past ListenAndServe's return.
+func TestServer_ListenAndServe_closesActiveConnsOnCancel(t *testing.T) {
+ port := findFreePortPair(t)
+ srv, err := NewServer(
+ WithLogger(NewTestLogger()),
+ WithInterface("127.0.0.1"),
+ WithPort(port),
+ )
+ require.NoError(t, err)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ errCh := make(chan error, 1)
+ go func() { errCh <- srv.ListenAndServe(ctx) }()
+
+ // Give the listeners a moment to start.
+ time.Sleep(100 * time.Millisecond)
+
+ // A session connection that stalls mid-handshake and a file transfer connection that never
+ // sends its 16-byte header: both park their serving goroutines in blocking reads.
+ sessionConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
+ require.NoError(t, err)
+ defer func() { _ = sessionConn.Close() }()
+
+ transferConn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port+1))
+ require.NoError(t, err)
+ defer func() { _ = transferConn.Close() }()
+
+ // Give the accept loops a moment to hand the connections to their goroutines.
+ time.Sleep(100 * time.Millisecond)
+
+ cancel()
+
+ select {
+ case <-errCh:
+ case <-time.After(2 * time.Second):
+ t.Fatal("ListenAndServe did not return after context cancellation with active connections")
+ }
+
+ // Both connections must have been closed by the server: reads unblock with EOF.
+ for _, conn := range []net.Conn{sessionConn, transferConn} {
+ require.NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second)))
+ _, err = conn.Read(make([]byte, 1))
+ assert.Error(t, err)
+ assert.NotErrorIs(t, err, os.ErrDeadlineExceeded)
+ }
+}
+
func TestServer_Shutdown_stopsListenAndServe(t *testing.T) {
srv, err := NewServer(
WithLogger(NewTestLogger()),