navidrome/plugins/host_websocket_test.go
Kendall Garner 0cd15c1ddc
feat(prometheus): add metrics to Subsonic API and Plugins (#4266)
* Add prometheus metrics to subsonic and plugins

* address feedback, do not log error if operation is not supported

* add missing timestamp and client to stats

* remove .view from subsonic route

* directly inject DataStore in Prometheus, to avoid having to pass it in every call

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
2025-06-27 22:13:57 -04:00

226 lines
5.8 KiB
Go

package plugins
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"sync"
"time"
gorillaws "github.com/gorilla/websocket"
"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 *Manager
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, nil)
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() {
// 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"))
})
})
})