package handlers import ( "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/go-chi/chi/v5" "github.com/gorilla/websocket" apimw "github.com/Silo-Server/silo-server/internal/api/middleware" "github.com/Silo-Server/silo-server/internal/auth" "github.com/Silo-Server/silo-server/internal/playback" ) func TestHandleSessionWebSocket_RequiresHelloBeforeRealtimeReady(t *testing.T) { sessionMgr := playback.NewSessionManager(0, 0) session, err := sessionMgr.StartSession(1, "profile-1", 100, playback.PlayDirect, false) if err != nil { t.Fatalf("StartSession: %v", err) } handler := NewPlaybackHandler(sessionMgr) handler.RealtimeHub = playback.NewRealtimeHub() router := chi.NewRouter() router.Get("/playback/ws/{session_id}", func(w http.ResponseWriter, r *http.Request) { ctx := apimw.SetClaims(r.Context(), &auth.Claims{UserID: 1, Role: "user", TokenType: auth.TokenTypeAccess}) handler.HandleSessionWebSocket(w, r.WithContext(ctx)) }) server := httptest.NewServer(router) defer server.Close() wsURL := "ws" + strings.TrimPrefix(server.URL, "http") + "/playback/ws/" + session.ID conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil) if err != nil { t.Fatalf("Dial websocket: %v", err) } defer conn.Close() got, err := sessionMgr.GetSession(session.ID) if err != nil { t.Fatalf("GetSession before hello: %v", err) } if got.HasRealtimeConnection { t.Fatal("session should not be realtime-ready before hello") } if got.HasWebSocket { t.Fatal("session should not report playback control before hello") } if err := conn.WriteJSON(playback.HelloEnvelope{ Type: playback.RealtimeMessageTypeHello, SessionID: session.ID, Client: playback.HelloClientInfo{ Name: "ios", Version: "1.0.0", }, Capabilities: playback.HelloCapabilities{ Commands: []playback.CommandName{ playback.CommandPause, playback.CommandUnpause, playback.CommandStop, playback.CommandTerminate, }, }, }); err != nil { t.Fatalf("WriteJSON hello: %v", err) } waitForPlaybackRealtimeState(t, sessionMgr, session.ID, true) if err := conn.Close(); err != nil { t.Fatalf("Close websocket: %v", err) } waitForPlaybackRealtimeState(t, sessionMgr, session.ID, false) } func waitForPlaybackRealtimeState(t *testing.T, sessionMgr *playback.SessionManager, sessionID string, want bool) { t.Helper() deadline := time.Now().Add(time.Second) for time.Now().Before(deadline) { session, err := sessionMgr.GetSession(sessionID) if err == nil && session != nil && session.HasRealtimeConnection == want && session.HasWebSocket == want { return } time.Sleep(10 * time.Millisecond) } session, err := sessionMgr.GetSession(sessionID) if err != nil { t.Fatalf("GetSession after wait: %v", err) } t.Fatalf( "session realtime state = %v/%v, want %v/%v", session.HasRealtimeConnection, session.HasWebSocket, want, want, ) }