knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/websocket/connection_test.go (about)

     1  /*
     2  Copyright 2019 The Knative Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package websocket
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"io"
    23  	"net/http"
    24  	"net/http/httptest"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	ktesting "knative.dev/pkg/logging/testing"
    30  
    31  	"k8s.io/apimachinery/pkg/util/wait"
    32  
    33  	"github.com/gorilla/websocket"
    34  )
    35  
    36  const propagationTimeout = 5 * time.Second
    37  
    38  type inspectableConnection struct {
    39  	nextReaderCalls      chan struct{}
    40  	writeMessageCalls    chan struct{}
    41  	closeCalls           chan struct{}
    42  	setReadDeadlineCalls chan struct{}
    43  	setPongHandlerCalls  chan struct{}
    44  
    45  	nextReaderFunc func() (int, io.Reader, error)
    46  }
    47  
    48  func (c *inspectableConnection) WriteMessage(messageType int, data []byte) error {
    49  	if c.writeMessageCalls != nil {
    50  		c.writeMessageCalls <- struct{}{}
    51  	}
    52  	return nil
    53  }
    54  
    55  func (c *inspectableConnection) NextReader() (int, io.Reader, error) {
    56  	if c.nextReaderCalls != nil {
    57  		c.nextReaderCalls <- struct{}{}
    58  	}
    59  	return c.nextReaderFunc()
    60  }
    61  
    62  func (c *inspectableConnection) Close() error {
    63  	if c.closeCalls != nil {
    64  		c.closeCalls <- struct{}{}
    65  	}
    66  	return nil
    67  }
    68  
    69  func (c *inspectableConnection) SetReadDeadline(deadline time.Time) error {
    70  	if c.setReadDeadlineCalls != nil {
    71  		c.setReadDeadlineCalls <- struct{}{}
    72  	}
    73  	return nil
    74  }
    75  
    76  func (c *inspectableConnection) SetPongHandler(func(string) error) {
    77  	if c.setPongHandlerCalls != nil {
    78  		c.setPongHandlerCalls <- struct{}{}
    79  	}
    80  }
    81  
    82  // staticConnFactory returns a static connection, for example
    83  // an inspectable connection.
    84  func staticConnFactory(conn rawConnection) func() (rawConnection, error) {
    85  	return func() (rawConnection, error) {
    86  		return conn, nil
    87  	}
    88  }
    89  
    90  // errConnFactory returns a static error.
    91  func errConnFactory(err error) func() (rawConnection, error) {
    92  	return func() (rawConnection, error) {
    93  		return nil, err
    94  	}
    95  }
    96  
    97  func TestRetriesWhileConnect(t *testing.T) {
    98  	const wantConnects = 2
    99  	gotConnects := 0
   100  
   101  	spy := &inspectableConnection{
   102  		closeCalls:           make(chan struct{}, 1),
   103  		setReadDeadlineCalls: make(chan struct{}, 1),
   104  		setPongHandlerCalls:  make(chan struct{}, 1),
   105  	}
   106  
   107  	connFactory := func() (rawConnection, error) {
   108  		gotConnects++
   109  		if gotConnects == wantConnects {
   110  			return spy, nil
   111  		}
   112  		return nil, errors.New("not yet")
   113  	}
   114  	conn := newConnection(connFactory, nil)
   115  
   116  	conn.connect()
   117  	conn.Shutdown()
   118  
   119  	if gotConnects != wantConnects {
   120  		t.Fatalf("Wanted %v retries. Got %v.", wantConnects, gotConnects)
   121  	}
   122  
   123  	// We want a readDeadline and a pongHandler to be set on the final connection.
   124  	if got, want := len(spy.setReadDeadlineCalls), 1; got != want {
   125  		t.Fatalf("Got %d 'SetReadDeadline' calls, want %d", got, want)
   126  	}
   127  	if got, want := len(spy.setPongHandlerCalls), 1; got != want {
   128  		t.Fatalf("Got %d 'SetPongHandler' calls, want %d", got, want)
   129  	}
   130  
   131  	if len(spy.closeCalls) != 1 {
   132  		t.Fatal("Wanted 'Close' to be called once, but got", len(spy.closeCalls))
   133  	}
   134  }
   135  
   136  func TestSendErrorOnNoConnection(t *testing.T) {
   137  	want := ErrConnectionNotEstablished
   138  
   139  	conn := &ManagedConnection{}
   140  	got := conn.Send("test")
   141  
   142  	if !errors.Is(got, want) {
   143  		t.Fatalf("Wanted error to be %v, but it was %v.", want, got)
   144  	}
   145  }
   146  
   147  func TestStatusOnNoConnection(t *testing.T) {
   148  	want := ErrConnectionNotEstablished
   149  
   150  	conn := &ManagedConnection{}
   151  	got := conn.Status()
   152  
   153  	if !errors.Is(got, want) {
   154  		t.Fatalf("Wanted error to be %v, but it was %v.", want, got)
   155  	}
   156  }
   157  
   158  func TestSendErrorOnEncode(t *testing.T) {
   159  	spy := &inspectableConnection{
   160  		writeMessageCalls: make(chan struct{}, 1),
   161  	}
   162  	conn := newConnection(staticConnFactory(spy), nil)
   163  	conn.connect()
   164  	// gob cannot encode nil values
   165  	got := conn.Send(nil)
   166  
   167  	if got == nil {
   168  		t.Fatal("Expected an error but got none")
   169  	}
   170  	if len(spy.writeMessageCalls) != 0 {
   171  		t.Fatalf("Expected 'WriteMessage' not to be called, but was called %v times", spy.writeMessageCalls)
   172  	}
   173  }
   174  
   175  func TestSendMessage(t *testing.T) {
   176  	spy := &inspectableConnection{
   177  		writeMessageCalls: make(chan struct{}, 1),
   178  	}
   179  	conn := newConnection(staticConnFactory(spy), nil)
   180  	conn.connect()
   181  
   182  	if got := conn.Status(); got != nil {
   183  		t.Errorf("Status() = %v, wanted nil", got)
   184  	}
   185  
   186  	if got := conn.Send("test"); got != nil {
   187  		t.Fatalf("Expected no error but got: %+v", got)
   188  	}
   189  	if len(spy.writeMessageCalls) != 1 {
   190  		t.Fatalf("Expected 'WriteMessage' to be called once, but was called %v times", spy.writeMessageCalls)
   191  	}
   192  }
   193  
   194  func TestSendRawMessage(t *testing.T) {
   195  	spy := &inspectableConnection{
   196  		writeMessageCalls: make(chan struct{}, 1),
   197  	}
   198  	conn := newConnection(staticConnFactory(spy), nil)
   199  	conn.connect()
   200  
   201  	if got := conn.Status(); got != nil {
   202  		t.Errorf("Status() = %v, wanted nil", got)
   203  	}
   204  
   205  	if got := conn.SendRaw(websocket.BinaryMessage, []byte("test")); got != nil {
   206  		t.Fatalf("Expected no error but got: %+v", got)
   207  	}
   208  	if len(spy.writeMessageCalls) != 1 {
   209  		t.Fatalf("Expected 'WriteMessage' to be called once, but was called %v times", spy.writeMessageCalls)
   210  	}
   211  }
   212  
   213  func TestReceiveMessage(t *testing.T) {
   214  	testMessage := "testmessage"
   215  
   216  	spy := &inspectableConnection{
   217  		writeMessageCalls: make(chan struct{}, 1),
   218  		nextReaderCalls:   make(chan struct{}, 1),
   219  		nextReaderFunc: func() (int, io.Reader, error) {
   220  			return websocket.TextMessage, strings.NewReader(testMessage), nil
   221  		},
   222  	}
   223  
   224  	messageChan := make(chan []byte, 1)
   225  	conn := newConnection(staticConnFactory(spy), messageChan)
   226  	conn.connect()
   227  	go conn.keepalive()
   228  
   229  	got := <-messageChan
   230  
   231  	if string(got) != testMessage {
   232  		t.Errorf("Received the wrong message, wanted %q, got %q", testMessage, string(got))
   233  	}
   234  }
   235  
   236  func TestCloseClosesConnection(t *testing.T) {
   237  	spy := &inspectableConnection{
   238  		closeCalls: make(chan struct{}, 1),
   239  	}
   240  	conn := newConnection(staticConnFactory(spy), nil)
   241  	conn.connect()
   242  	conn.Shutdown()
   243  
   244  	if len(spy.closeCalls) != 1 {
   245  		t.Fatal("Expected 'Close' to be called once, got", len(spy.closeCalls))
   246  	}
   247  }
   248  
   249  func TestCloseIgnoresNoConnection(t *testing.T) {
   250  	conn := &ManagedConnection{
   251  		closeChan: make(chan struct{}, 1),
   252  	}
   253  	got := conn.Shutdown()
   254  
   255  	if got != nil {
   256  		t.Fatal("Expected no error, got", got)
   257  	}
   258  }
   259  
   260  func TestConnectFailureReturnsError(t *testing.T) {
   261  	conn := newConnection(errConnFactory(ErrConnectionNotEstablished), nil)
   262  
   263  	// Shorten the connection backoff duration for this test
   264  	conn.connectionBackoff.Duration = 1 * time.Millisecond
   265  
   266  	got := conn.connect()
   267  
   268  	if got == nil {
   269  		t.Fatal("Expected an error but got none")
   270  	}
   271  }
   272  
   273  func TestKeepaliveWithNoConnectionReturnsError(t *testing.T) {
   274  	conn := newConnection(nil, nil)
   275  	got := conn.keepalive()
   276  
   277  	if got == nil {
   278  		t.Fatal("Expected an error but got none")
   279  	}
   280  }
   281  
   282  func TestConnectLoopIsStopped(t *testing.T) {
   283  	conn := newConnection(errConnFactory(errors.New("connection error")), nil)
   284  
   285  	errorChan := make(chan error)
   286  	go func() {
   287  		errorChan <- conn.connect()
   288  	}()
   289  
   290  	conn.Shutdown()
   291  
   292  	select {
   293  	case err := <-errorChan:
   294  		if !errors.Is(err, errShuttingDown) {
   295  			t.Errorf("Wrong 'connect' error, got %v, want %v", err, errShuttingDown)
   296  		}
   297  	case <-time.After(propagationTimeout):
   298  		t.Error("Timed out waiting for the keepalive loop to stop.")
   299  	}
   300  }
   301  
   302  func TestKeepaliveLoopIsStopped(t *testing.T) {
   303  	spy := &inspectableConnection{
   304  		nextReaderFunc: func() (int, io.Reader, error) {
   305  			return websocket.TextMessage, nil, nil
   306  		},
   307  	}
   308  	conn := newConnection(staticConnFactory(spy), nil)
   309  	conn.connect()
   310  
   311  	errorChan := make(chan error)
   312  	go func() {
   313  		errorChan <- conn.keepalive()
   314  	}()
   315  
   316  	conn.Shutdown()
   317  
   318  	select {
   319  	case err := <-errorChan:
   320  		if !errors.Is(err, errShuttingDown) {
   321  			t.Errorf("Wrong 'keepalive' error, got %v, want %v", err, errShuttingDown)
   322  		}
   323  	case <-time.After(propagationTimeout):
   324  		t.Error("Timed out waiting for the keepalive loop to stop.")
   325  	}
   326  }
   327  
   328  func TestDoubleShutdown(t *testing.T) {
   329  	spy := &inspectableConnection{
   330  		closeCalls: make(chan struct{}, 2), // potentially allow 2 calls
   331  	}
   332  	conn := newConnection(staticConnFactory(spy), nil)
   333  	conn.connect()
   334  	conn.Shutdown()
   335  	conn.Shutdown()
   336  
   337  	if want, got := 1, len(spy.closeCalls); want != got {
   338  		t.Errorf("Wrong 'Close' callcount, got %d, want %d", got, want)
   339  	}
   340  }
   341  
   342  func TestDurableConnectionWhenConnectionBreaksDown(t *testing.T) {
   343  	const testPayload = "test"
   344  	reconnectChan := make(chan struct{})
   345  
   346  	upgrader := websocket.Upgrader{}
   347  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   348  		c, err := upgrader.Upgrade(w, r, nil)
   349  		if err != nil {
   350  			return
   351  		}
   352  
   353  		// Waits for a message to be sent before dropping the connection.
   354  		<-reconnectChan
   355  		c.Close()
   356  	}))
   357  	defer s.Close()
   358  
   359  	logger := ktesting.TestLogger(t)
   360  	target := "ws" + strings.TrimPrefix(s.URL, "http")
   361  	conn := NewDurableSendingConnection(target, logger)
   362  	defer conn.Shutdown()
   363  
   364  	for range 10 {
   365  		err := wait.PollUntilContextTimeout(context.Background(), 50*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) {
   366  			if err := conn.Send(testPayload); err != nil {
   367  				return false, nil //nolint:nilerr
   368  			}
   369  			return true, nil
   370  		})
   371  		if err != nil {
   372  			t.Error("Timed out trying to send a message:", err)
   373  		}
   374  
   375  		// Message successfully sent, instruct the server to drop the connection.
   376  		reconnectChan <- struct{}{}
   377  	}
   378  }
   379  
   380  func TestDurableConnectionSendsPingsRegularly(t *testing.T) {
   381  	// Reset pongTimeout to something quite short.
   382  	pingTimeoutBackup := pongTimeout
   383  	pongTimeout = 100 * time.Millisecond
   384  	t.Cleanup(func() {
   385  		pongTimeout = pingTimeoutBackup
   386  	})
   387  
   388  	upgrader := websocket.Upgrader{}
   389  
   390  	pingReceived := make(chan struct{})
   391  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   392  		c, err := upgrader.Upgrade(w, r, nil)
   393  		if err != nil {
   394  			return
   395  		}
   396  
   397  		c.SetPingHandler(func(_ string) error {
   398  			pingReceived <- struct{}{}
   399  			return c.WriteMessage(websocket.PongMessage, []byte{})
   400  		})
   401  
   402  		for {
   403  			_, _, err := c.ReadMessage()
   404  			if err != nil {
   405  				break
   406  			}
   407  		}
   408  	}))
   409  	defer s.Close()
   410  
   411  	logger := ktesting.TestLogger(t)
   412  	target := "ws" + strings.TrimPrefix(s.URL, "http")
   413  	conn := NewDurableSendingConnection(target, logger)
   414  	defer conn.Shutdown()
   415  
   416  	// Wait for 5 pings to be received by the server.
   417  	for range 5 {
   418  		<-pingReceived
   419  	}
   420  }
   421  
   422  func TestOnConnectAndOnDisconnectCallbacks(t *testing.T) {
   423  	reconnectChan := make(chan struct{})
   424  
   425  	upgrader := websocket.Upgrader{}
   426  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   427  		c, err := upgrader.Upgrade(w, r, nil)
   428  		if err != nil {
   429  			return
   430  		}
   431  
   432  		// Wait for signal to drop the connection.
   433  		<-reconnectChan
   434  		c.Close()
   435  	}))
   436  	defer s.Close()
   437  
   438  	logger := ktesting.TestLogger(t)
   439  	target := "ws" + strings.TrimPrefix(s.URL, "http")
   440  
   441  	onConnectCalled := make(chan struct{}, 10)
   442  	onDisconnectCalled := make(chan error, 10)
   443  
   444  	conn := NewDurableSendingConnection(target, logger,
   445  		WithOnConnect(func() {
   446  			onConnectCalled <- struct{}{}
   447  		}),
   448  		WithOnDisconnect(func(err error) {
   449  			onDisconnectCalled <- err
   450  		}),
   451  	)
   452  	defer conn.Shutdown()
   453  
   454  	// Wait for the first OnConnect call
   455  	select {
   456  	case <-onConnectCalled:
   457  		// Success - OnConnect was called
   458  	case <-time.After(propagationTimeout):
   459  		t.Fatal("Timed out waiting for OnConnect to be called")
   460  	}
   461  
   462  	// Trigger a disconnect by closing the server-side connection
   463  	reconnectChan <- struct{}{}
   464  
   465  	// Wait for OnDisconnect to be called
   466  	select {
   467  	case err := <-onDisconnectCalled:
   468  		if err == nil {
   469  			t.Error("Expected OnDisconnect to receive an error, got nil")
   470  		}
   471  	case <-time.After(propagationTimeout):
   472  		t.Fatal("Timed out waiting for OnDisconnect to be called")
   473  	}
   474  
   475  	// Wait for reconnection (OnConnect should be called again)
   476  	select {
   477  	case <-onConnectCalled:
   478  		// Success - OnConnect was called again after reconnection
   479  	case <-time.After(propagationTimeout):
   480  		t.Fatal("Timed out waiting for OnConnect to be called after reconnection")
   481  	}
   482  }
   483  
   484  func TestOnConnectAndOnDisconnectCallbacksNotSet(t *testing.T) {
   485  	reconnectChan := make(chan struct{})
   486  
   487  	upgrader := websocket.Upgrader{}
   488  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   489  		c, err := upgrader.Upgrade(w, r, nil)
   490  		if err != nil {
   491  			return
   492  		}
   493  
   494  		// Wait for signal to drop the connection.
   495  		<-reconnectChan
   496  		c.Close()
   497  	}))
   498  	defer s.Close()
   499  
   500  	logger := ktesting.TestLogger(t)
   501  	target := "ws" + strings.TrimPrefix(s.URL, "http")
   502  
   503  	// Create connection without setting callbacks - should not panic
   504  	conn := NewDurableSendingConnection(target, logger)
   505  	defer conn.Shutdown()
   506  
   507  	// Wait for connection to be established
   508  	err := wait.PollUntilContextTimeout(context.Background(), 50*time.Millisecond, propagationTimeout, true, func(ctx context.Context) (bool, error) {
   509  		return conn.Status() == nil, nil
   510  	})
   511  	if err != nil {
   512  		t.Fatal("Timed out waiting for connection to be established:", err)
   513  	}
   514  
   515  	// Trigger disconnect - should not panic even without callbacks
   516  	reconnectChan <- struct{}{}
   517  
   518  	// Wait a bit and verify no panic occurred
   519  	time.Sleep(100 * time.Millisecond)
   520  }
   521  
   522  func TestNewDurableSendingConnectionGuaranteed(t *testing.T) {
   523  	// Unhappy case.
   524  	logger := ktesting.TestLogger(t)
   525  	_, err := NewDurableSendingConnectionGuaranteed("ws://somewhere.not.exist", time.Second, logger)
   526  	if got, want := err.Error(), ErrConnectionNotEstablished.Error(); got != want {
   527  		t.Errorf("Got error: %v, want error: %v", got, want)
   528  	}
   529  
   530  	// Happy case.
   531  	const testPayload = "test"
   532  	reconnectChan := make(chan struct{})
   533  	upgrader := websocket.Upgrader{}
   534  	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   535  		c, err := upgrader.Upgrade(w, r, nil)
   536  		if err != nil {
   537  			return
   538  		}
   539  
   540  		// Waits for a message to be sent before dropping the connection.
   541  		<-reconnectChan
   542  		c.Close()
   543  	}))
   544  	defer s.Close()
   545  
   546  	target := "ws" + strings.TrimPrefix(s.URL, "http")
   547  	conn, err := NewDurableSendingConnectionGuaranteed(target, time.Second, logger)
   548  	if err != nil {
   549  		t.Error("Got error from NewDurableSendingConnectionGuaranteed:", err)
   550  	}
   551  	defer conn.Shutdown()
   552  
   553  	// Sending the message immediately should be fine as the connection has been established.
   554  	if err := conn.Send(testPayload); err != nil {
   555  		t.Error("Failed to send a message:", err)
   556  	}
   557  
   558  	// Message successfully sent, instruct the server to drop the connection.
   559  	reconnectChan <- struct{}{}
   560  }