Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
232 lines
6.0 KiB
Go
232 lines
6.0 KiB
Go
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
gorillaws "github.com/gorilla/websocket"
|
|
"github.com/navidrome/navidrome/core/metrics"
|
|
"github.com/navidrome/navidrome/plugins/host/websocket"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("WebSocket Host Service", func() {
|
|
var (
|
|
wsService *websocketService
|
|
manager *managerImpl
|
|
ctx context.Context
|
|
server *httptest.Server
|
|
upgrader gorillaws.Upgrader
|
|
serverMessages []string
|
|
serverMu sync.Mutex
|
|
)
|
|
|
|
// WebSocket echo server handler
|
|
echoHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
// Check headers
|
|
if r.Header.Get("X-Test-Header") != "test-value" {
|
|
http.Error(w, "Missing or invalid X-Test-Header", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Upgrade connection to WebSocket
|
|
conn, err := upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Echo messages back
|
|
for {
|
|
mt, message, err := conn.ReadMessage()
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
// Store the received message for verification
|
|
if mt == gorillaws.TextMessage {
|
|
msg := string(message)
|
|
serverMu.Lock()
|
|
serverMessages = append(serverMessages, msg)
|
|
serverMu.Unlock()
|
|
}
|
|
|
|
// Echo it back
|
|
err = conn.WriteMessage(mt, message)
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
// If message is "close", close the connection
|
|
if mt == gorillaws.TextMessage && string(message) == "close" {
|
|
_ = conn.WriteControl(
|
|
gorillaws.CloseMessage,
|
|
gorillaws.FormatCloseMessage(gorillaws.CloseNormalClosure, "bye"),
|
|
time.Now().Add(time.Second),
|
|
)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
BeforeEach(func() {
|
|
ctx = context.Background()
|
|
serverMessages = make([]string, 0)
|
|
serverMu = sync.Mutex{}
|
|
|
|
// Create a test WebSocket server
|
|
//upgrader = gorillaws.Upgrader{}
|
|
server = httptest.NewServer(http.HandlerFunc(echoHandler))
|
|
DeferCleanup(server.Close)
|
|
|
|
// Create a new manager and websocket service
|
|
manager = createManager(nil, metrics.NewNoopInstance())
|
|
wsService = newWebsocketService(manager)
|
|
})
|
|
|
|
Describe("WebSocket operations", func() {
|
|
var (
|
|
pluginName string
|
|
connectionID string
|
|
wsURL string
|
|
)
|
|
|
|
BeforeEach(func() {
|
|
pluginName = "test-plugin"
|
|
connectionID = "test-connection-id"
|
|
wsURL = "ws" + strings.TrimPrefix(server.URL, "http")
|
|
})
|
|
|
|
It("connects to a WebSocket server", func() {
|
|
// Connect to the WebSocket server
|
|
req := &websocket.ConnectRequest{
|
|
Url: wsURL,
|
|
Headers: map[string]string{
|
|
"X-Test-Header": "test-value",
|
|
},
|
|
ConnectionId: connectionID,
|
|
}
|
|
|
|
resp, err := wsService.connect(ctx, pluginName, req, nil)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(resp.ConnectionId).ToNot(BeEmpty())
|
|
connectionID = resp.ConnectionId
|
|
|
|
// Verify that the connection was added to the service
|
|
internalID := pluginName + ":" + connectionID
|
|
Expect(wsService.hasConnection(internalID)).To(BeTrue())
|
|
})
|
|
|
|
It("sends and receives text messages", func() {
|
|
// Connect to the WebSocket server
|
|
req := &websocket.ConnectRequest{
|
|
Url: wsURL,
|
|
Headers: map[string]string{
|
|
"X-Test-Header": "test-value",
|
|
},
|
|
ConnectionId: connectionID,
|
|
}
|
|
|
|
resp, err := wsService.connect(ctx, pluginName, req, nil)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
connectionID = resp.ConnectionId
|
|
|
|
// Send a text message
|
|
textReq := &websocket.SendTextRequest{
|
|
ConnectionId: connectionID,
|
|
Message: "hello websocket",
|
|
}
|
|
|
|
_, err = wsService.sendText(ctx, pluginName, textReq)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Wait a bit for the message to be processed
|
|
Eventually(func() []string {
|
|
serverMu.Lock()
|
|
defer serverMu.Unlock()
|
|
return serverMessages
|
|
}, "1s").Should(ContainElement("hello websocket"))
|
|
})
|
|
|
|
It("closes a WebSocket connection", func() {
|
|
// Connect to the WebSocket server
|
|
req := &websocket.ConnectRequest{
|
|
Url: wsURL,
|
|
Headers: map[string]string{
|
|
"X-Test-Header": "test-value",
|
|
},
|
|
ConnectionId: connectionID,
|
|
}
|
|
|
|
resp, err := wsService.connect(ctx, pluginName, req, nil)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
connectionID = resp.ConnectionId
|
|
|
|
initialCount := wsService.connectionCount()
|
|
|
|
// Close the connection
|
|
closeReq := &websocket.CloseRequest{
|
|
ConnectionId: connectionID,
|
|
Code: 1000, // Normal closure
|
|
Reason: "test complete",
|
|
}
|
|
|
|
_, err = wsService.close(ctx, pluginName, closeReq)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Verify that the connection was removed
|
|
Eventually(func() int {
|
|
return wsService.connectionCount()
|
|
}, "1s").Should(Equal(initialCount - 1))
|
|
|
|
internalID := pluginName + ":" + connectionID
|
|
Expect(wsService.hasConnection(internalID)).To(BeFalse())
|
|
})
|
|
|
|
It("handles connection errors gracefully", func() {
|
|
if testing.Short() {
|
|
GinkgoT().Skip("skipping test in short mode.")
|
|
}
|
|
|
|
// Try to connect to an invalid URL
|
|
req := &websocket.ConnectRequest{
|
|
Url: "ws://invalid-url-that-does-not-exist",
|
|
Headers: map[string]string{},
|
|
ConnectionId: connectionID,
|
|
}
|
|
|
|
_, err := wsService.connect(ctx, pluginName, req, nil)
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
|
|
It("returns error when attempting to use non-existent connection", func() {
|
|
// Try to send a message to a non-existent connection
|
|
textReq := &websocket.SendTextRequest{
|
|
ConnectionId: "non-existent-connection",
|
|
Message: "this should fail",
|
|
}
|
|
|
|
sendResp, err := wsService.sendText(ctx, pluginName, textReq)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(sendResp.Error).To(ContainSubstring("connection not found"))
|
|
|
|
// Try to close a non-existent connection
|
|
closeReq := &websocket.CloseRequest{
|
|
ConnectionId: "non-existent-connection",
|
|
Code: 1000,
|
|
Reason: "test complete",
|
|
}
|
|
|
|
closeResp, err := wsService.close(ctx, pluginName, closeReq)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(closeResp.Error).To(ContainSubstring("connection not found"))
|
|
})
|
|
})
|
|
})
|