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 }