mirror of
https://github.com/navidrome/navidrome.git
synced 2025-07-14 07:31:28 +03:00
* 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>
226 lines
5.8 KiB
Go
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"))
|
|
})
|
|
})
|
|
})
|