| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- package service
- import (
- "context"
- "errors"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "time"
- coderws "github.com/coder/websocket"
- "github.com/stretchr/testify/require"
- )
- func TestReadOpenAIWSClientMessage_ControlCloseFrames(t *testing.T) {
- tests := []struct {
- name string
- timeout time.Duration
- timeoutStatus coderws.StatusCode
- timeoutReason string
- cancelCause error
- wantStatus coderws.StatusCode
- wantReason string
- }{
- {
- name: "inter-turn idle sends normal close",
- timeout: 25 * time.Millisecond,
- timeoutStatus: coderws.StatusNormalClosure,
- timeoutReason: "websocket idle timeout",
- wantStatus: coderws.StatusNormalClosure,
- wantReason: "websocket idle timeout",
- },
- {
- name: "first message timeout sends policy close",
- timeout: 25 * time.Millisecond,
- timeoutStatus: coderws.StatusPolicyViolation,
- timeoutReason: "missing first response.create message",
- wantStatus: coderws.StatusPolicyViolation,
- wantReason: "missing first response.create message",
- },
- {
- name: "lease loss sends retry close",
- cancelCause: ErrOpenAIWSIngressLeaseLost,
- wantStatus: coderws.StatusTryAgainLater,
- wantReason: "websocket ingress capacity lease lost; please reconnect",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- controlCtx, cancelControl := context.WithCancelCause(context.Background())
- defer cancelControl(context.Canceled)
- serverResult := make(chan error, 1)
- readStarted := make(chan struct{})
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- conn, err := coderws.Accept(w, r, nil)
- if err != nil {
- serverResult <- err
- return
- }
- defer func() { _ = conn.CloseNow() }()
- close(readStarted)
- _, _, err = ReadOpenAIWSClientMessage(
- controlCtx,
- conn,
- tt.timeout,
- tt.timeoutStatus,
- tt.timeoutReason,
- )
- serverResult <- err
- }))
- defer server.Close()
- dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Second)
- clientConn, _, err := coderws.Dial(dialCtx, "ws"+strings.TrimPrefix(server.URL, "http"), nil)
- cancelDial()
- require.NoError(t, err)
- defer func() { _ = clientConn.CloseNow() }()
- <-readStarted
- if tt.cancelCause != nil {
- cancelControl(tt.cancelCause)
- }
- readCtx, cancelRead := context.WithTimeout(context.Background(), time.Second)
- _, _, err = clientConn.Read(readCtx)
- cancelRead()
- var clientClose coderws.CloseError
- require.ErrorAs(t, err, &clientClose)
- require.Equal(t, tt.wantStatus, clientClose.Code)
- require.Equal(t, tt.wantReason, clientClose.Reason)
- select {
- case serverErr := <-serverResult:
- var closeErr *OpenAIWSClientCloseError
- require.ErrorAs(t, serverErr, &closeErr)
- require.Equal(t, tt.wantStatus, closeErr.StatusCode())
- require.Equal(t, tt.wantReason, closeErr.Reason())
- case <-time.After(time.Second):
- t.Fatal("server read goroutine did not exit after close handshake")
- }
- })
- }
- }
- func TestReadOpenAIWSClientMessage_ParentCancellationStillJoinsRead(t *testing.T) {
- controlCtx, cancelControl := context.WithCancelCause(context.Background())
- serverResult := make(chan error, 1)
- readStarted := make(chan struct{})
- server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- conn, err := coderws.Accept(w, r, nil)
- if err != nil {
- serverResult <- err
- return
- }
- defer func() { _ = conn.CloseNow() }()
- close(readStarted)
- _, _, err = ReadOpenAIWSClientMessage(controlCtx, conn, 0, 0, "")
- serverResult <- err
- }))
- defer server.Close()
- dialCtx, cancelDial := context.WithTimeout(context.Background(), time.Second)
- clientConn, _, err := coderws.Dial(dialCtx, "ws"+strings.TrimPrefix(server.URL, "http"), nil)
- cancelDial()
- require.NoError(t, err)
- defer func() { _ = clientConn.CloseNow() }()
- <-readStarted
- cancelControl(errors.New("server shutting down"))
- readCtx, cancelRead := context.WithTimeout(context.Background(), time.Second)
- _, _, err = clientConn.Read(readCtx)
- cancelRead()
- var clientClose coderws.CloseError
- require.ErrorAs(t, err, &clientClose)
- require.Equal(t, coderws.StatusGoingAway, clientClose.Code)
- require.Equal(t, "websocket request canceled", clientClose.Reason)
- select {
- case <-serverResult:
- case <-time.After(time.Second):
- t.Fatal("server read goroutine leaked after parent cancellation")
- }
- }
|