package ynison import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/client" "gitea.mrixs.me/Mrixs/yandex-music-api-go/internal/ynison/proto" "github.com/gorilla/websocket" ) // TestYnisonClient_Connect tests the Connect method with a mock WebSocket server func TestYnisonClient_Connect(t *testing.T) { // Create a mock WebSocket server var receivedMessages [][]byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { t.Logf("Upgrade error: %v", err) return } defer conn.Close() // Read messages from client for { _, message, err := conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { t.Logf("Read error: %v", err) } return } receivedMessages = append(receivedMessages, message) } })) defer server.Close() // Convert HTTP URL to WS URL wsURL := "ws" + server.URL[4:] // Replace http with ws // Create a client with test token c := client.NewClient(client.ClientConfig{ Token: "test-token", }) config := DeviceConfig{ DeviceID: "test-device-123", Title: "Test Device", DeviceType: proto.DeviceType_DEVICE_TYPE_WEB_DESKTOP, AppName: "TestApp", AppVersion: "1.0.0", CanBePlayer: true, CanBeRemote: true, VolumeGran: 100, } ynisonClient := NewYnisonClient(c, config) // Override the wsURL for testing ynisonClient.overrideWSURL(wsURL) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err := ynisonClient.Connect(ctx) if err != nil { t.Fatalf("Connect failed: %v", err) } if !ynisonClient.IsConnected() { t.Error("Expected client to be connected after Connect") } // Wait a bit for the initial message to be sent time.Sleep(100 * time.Millisecond) // Verify that an UpdateFullState message was sent if len(receivedMessages) == 0 { t.Error("Expected at least one message to be sent") } else { var req proto.PutYnisonStateRequest if err := json.Unmarshal(receivedMessages[0], &req); err != nil { t.Fatalf("Failed to unmarshal sent message: %v", err) } if req.UpdateFullState == nil { t.Error("Expected UpdateFullState in first message") } if req.UpdateFullState.Device == nil { t.Error("Expected Device in UpdateFullState") } if req.UpdateFullState.Device.Info.DeviceId != "test-device-123" { t.Errorf("Expected DeviceId 'test-device-123', got '%s'", req.UpdateFullState.Device.Info.DeviceId) } } // Cleanup err = ynisonClient.Disconnect() if err != nil { t.Errorf("Disconnect failed: %v", err) } if ynisonClient.IsConnected() { t.Error("Expected client to be disconnected after Disconnect") } } // TestYnisonClient_Disconnect tests the Disconnect method func TestYnisonClient_Disconnect(t *testing.T) { // Create a mock WebSocket server server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() // Keep connection open until client closes it for { _, _, err := conn.ReadMessage() if err != nil { return } } })) defer server.Close() wsURL := "ws" + server.URL[4:] c := client.NewClient(client.ClientConfig{ Token: "test-token", }) config := DeviceConfig{ DeviceID: "test-device-disconnect", } ynisonClient := NewYnisonClient(c, config) ynisonClient.overrideWSURL(wsURL) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err := ynisonClient.Connect(ctx) if err != nil { t.Fatalf("Connect failed: %v", err) } if !ynisonClient.IsConnected() { t.Error("Expected client to be connected") } err = ynisonClient.Disconnect() if err != nil { t.Errorf("Disconnect failed: %v", err) } if ynisonClient.IsConnected() { t.Error("Expected client to be disconnected after Disconnect") } // Calling Disconnect again should not error err = ynisonClient.Disconnect() if err != nil { t.Errorf("Second Disconnect should not error: %v", err) } } // TestYnisonClient_UpdatePlayingStatus tests the UpdatePlayingStatus method func TestYnisonClient_UpdatePlayingStatus(t *testing.T) { var receivedMessages [][]byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { upgrader := websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, } conn, err := upgrader.Upgrade(w, r, nil) if err != nil { return } defer conn.Close() for { _, message, err := conn.ReadMessage() if err != nil { return } receivedMessages = append(receivedMessages, message) } })) defer server.Close() wsURL := "ws" + server.URL[4:] c := client.NewClient(client.ClientConfig{ Token: "test-token", }) config := DeviceConfig{ DeviceID: "test-device-status", } ynisonClient := NewYnisonClient(c, config) ynisonClient.overrideWSURL(wsURL) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() err := ynisonClient.Connect(ctx) if err != nil { t.Fatalf("Connect failed: %v", err) } // Wait for initial connection message time.Sleep(100 * time.Millisecond) initialMsgCount := len(receivedMessages) // Create a playing status status := CreatePlayingStatus(50000, 200000, false, 1.0, "test-device-status") // Update playing status err = ynisonClient.UpdatePlayingStatus(ctx, status) if err != nil { t.Fatalf("UpdatePlayingStatus failed: %v", err) } // Wait for message to be sent time.Sleep(100 * time.Millisecond) // Verify the UpdatePlayingStatus message was sent if len(receivedMessages) <= initialMsgCount { t.Error("Expected UpdatePlayingStatus message to be sent") } else { var req proto.PutYnisonStateRequest if err := json.Unmarshal(receivedMessages[initialMsgCount], &req); err != nil { t.Fatalf("Failed to unmarshal sent message: %v", err) } if req.UpdatePlayingStatus == nil { t.Error("Expected UpdatePlayingStatus in message") } if req.UpdatePlayingStatus.PlayingStatus == nil { t.Error("Expected PlayingStatus in UpdatePlayingStatus") } if req.UpdatePlayingStatus.PlayingStatus.ProgressMs != 50000 { t.Errorf("Expected ProgressMs 50000, got %d", req.UpdatePlayingStatus.PlayingStatus.ProgressMs) } if req.UpdatePlayingStatus.PlayingStatus.DurationMs != 200000 { t.Errorf("Expected DurationMs 200000, got %d", req.UpdatePlayingStatus.PlayingStatus.DurationMs) } if req.UpdatePlayingStatus.PlayingStatus.Paused != false { t.Errorf("Expected Paused false, got %v", req.UpdatePlayingStatus.PlayingStatus.Paused) } if req.UpdatePlayingStatus.PlayingStatus.PlaybackSpeed != 1.0 { t.Errorf("Expected PlaybackSpeed 1.0, got %f", req.UpdatePlayingStatus.PlayingStatus.PlaybackSpeed) } if req.UpdatePlayingStatus.PlayingStatus.Version.DeviceId != "test-device-status" { t.Errorf("Expected DeviceId 'test-device-status', got '%s'", req.UpdatePlayingStatus.PlayingStatus.Version.DeviceId) } if req.Rid == "" { t.Error("Expected non-empty Rid") } } err = ynisonClient.Disconnect() if err != nil { t.Errorf("Disconnect failed: %v", err) } } // TestYnisonClient_UpdatePlayingStatus_NotConnected tests UpdatePlayingStatus when not connected func TestYnisonClient_UpdatePlayingStatus_NotConnected(t *testing.T) { c := client.NewClient(client.ClientConfig{ Token: "test-token", }) config := DeviceConfig{ DeviceID: "test-device-not-connected", } ynisonClient := NewYnisonClient(c, config) status := CreatePlayingStatus(50000, 200000, false, 1.0, "test-device-not-connected") err := ynisonClient.UpdatePlayingStatus(context.Background(), status) if err == nil { t.Error("Expected error when not connected") } if err != nil && err.Error() != "not connected" { t.Errorf("Expected 'not connected' error, got: %v", err) } } // TestYnisonClient_Connect_NoToken tests Connect without a token func TestYnisonClient_Connect_NoToken(t *testing.T) { c := client.NewClient(client.ClientConfig{ Token: "", }) config := DeviceConfig{ DeviceID: "test-device-no-token", } ynisonClient := NewYnisonClient(c, config) err := ynisonClient.Connect(context.Background()) if err == nil { t.Error("Expected error when no token") } if err != nil && err.Error() != "no auth token available" { t.Errorf("Expected 'no auth token available' error, got: %v", err) } } // TestYnisonClient_IsConnected tests the IsConnected method func TestYnisonClient_IsConnected(t *testing.T) { c := client.NewClient(client.ClientConfig{ Token: "test-token", }) config := DeviceConfig{ DeviceID: "test-device-isconnected", } ynisonClient := NewYnisonClient(c, config) if ynisonClient.IsConnected() { t.Error("Expected client to not be connected initially") } // We can't easily test connected state without a real server, // but we verified this in the Connect/Disconnect tests } // TestYnisonClient_RegisterHandler tests handler registration func TestYnisonClient_RegisterHandler(t *testing.T) { c := client.NewClient(client.ClientConfig{ Token: "test-token", }) config := DeviceConfig{ DeviceID: "test-device-handler", } ynisonClient := NewYnisonClient(c, config) ynisonClient.RegisterHandler("test", func(resp *proto.PutYnisonStateResponse) {}) // We can't easily test handler invocation without a real server, // but we can verify the handler is registered ynisonClient.UnregisterHandler("test") // Re-register and verify it doesn't panic ynisonClient.RegisterHandler("test2", func(resp *proto.PutYnisonStateResponse) {}) ynisonClient.UnregisterHandler("test2") ynisonClient.UnregisterHandler("nonexistent") // Should not panic } // TestYnisonClient_DisconnectWithoutConnect tests Disconnect without prior Connect func TestYnisonClient_DisconnectWithoutConnect(t *testing.T) { c := client.NewClient(client.ClientConfig{ Token: "test-token", }) config := DeviceConfig{ DeviceID: "test-device-disconnect-no-connect", } ynisonClient := NewYnisonClient(c, config) // Should not panic or error err := ynisonClient.Disconnect() if err != nil { t.Errorf("Disconnect without connect should not error: %v", err) } }