test(plugins): speed up integration tests (~45% improvement) (#5137)

* test(plugins): speed up integration tests with shared wazero cache

Reduce plugin test suite runtime from ~22s to ~12s by:

- Creating a shared wazero compilation cache directory in TestPlugins()
  and setting conf.Server.CacheFolder globally so all test Manager
  instances reuse compiled WASM binaries from disk cache
- Moving 6 createTestManager* calls from inside It blocks to BeforeAll
  blocks in scrobbler_adapter_test.go and manager_call_test.go
- Replacing time.Sleep(2s) in KVStore TTL test with Eventually polling
- Reducing WebSocket callback sleeps from 100ms to 10ms

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

* test(plugins): enhance websocket tests by storing server messages for verification

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan Quintão
2026-03-02 16:18:30 -05:00
committed by GitHub
parent 82f9f88c0f
commit 30df004d4d
13 changed files with 165 additions and 135 deletions
-1
View File
@@ -49,7 +49,6 @@ var _ = Describe("ArtworkService", Ordered, func() {
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Initialize auth (required for token generation) // Initialize auth (required for token generation)
ds := &tests.MockDataStore{MockedProperty: &tests.MockedPropertyRepo{}} ds := &tests.MockDataStore{MockedProperty: &tests.MockedPropertyRepo{}}
-1
View File
@@ -345,7 +345,6 @@ var _ = Describe("CacheService Integration", Ordered, func() {
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Setup mock DataStore with pre-enabled plugin // Setup mock DataStore with pre-enabled plugin
mockPluginRepo := tests.CreateMockPluginRepo() mockPluginRepo := tests.CreateMockPluginRepo()
-1
View File
@@ -59,7 +59,6 @@ func setupTestConfigPlugin(configJSON string) (*Manager, func(context.Context, t
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Setup mock DataStore // Setup mock DataStore
mockPluginRepo := tests.CreateMockPluginRepo() mockPluginRepo := tests.CreateMockPluginRepo()
+6 -8
View File
@@ -677,7 +677,6 @@ var _ = Describe("KVStoreService Integration", Ordered, func() {
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
conf.Server.DataFolder = tmpDir conf.Server.DataFolder = tmpDir
// Setup mock DataStore with pre-enabled plugin // Setup mock DataStore with pre-enabled plugin
@@ -924,16 +923,15 @@ var _ = Describe("KVStoreService Integration", Ordered, func() {
Expect(output.Exists).To(BeTrue()) Expect(output.Exists).To(BeTrue())
Expect(output.Value).To(Equal([]byte("temporary"))) Expect(output.Value).To(Equal([]byte("temporary")))
// Wait for expiration // Poll until the key expires (1s TTL)
time.Sleep(2 * time.Second) Eventually(func(g Gomega) {
output, err := callTestKVStore(ctx, testKVStoreInput{
// Should no longer exist
output, err = callTestKVStore(ctx, testKVStoreInput{
Operation: "get", Operation: "get",
Key: "ttl_key", Key: "ttl_key",
}) })
Expect(err).ToNot(HaveOccurred()) g.Expect(err).ToNot(HaveOccurred())
Expect(output.Exists).To(BeFalse()) g.Expect(output.Exists).To(BeFalse())
}).WithTimeout(3 * time.Second).WithPolling(200 * time.Millisecond).Should(Succeed())
}) })
It("should delete keys by prefix", func() { It("should delete keys by prefix", func() {
-2
View File
@@ -264,7 +264,6 @@ var _ = Describe("LibraryService", Ordered, func() {
DeferCleanup(configtest.SetupConfig()) DeferCleanup(configtest.SetupConfig())
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Create mock &tests.MockLibraryRepo{} // Create mock &tests.MockLibraryRepo{}
mockLibRepo := &tests.MockLibraryRepo{} mockLibRepo := &tests.MockLibraryRepo{}
@@ -360,7 +359,6 @@ var _ = Describe("LibraryService Integration", Ordered, func() {
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Setup mock DataStore with pre-enabled plugin and library // Setup mock DataStore with pre-enabled plugin and library
mockPluginRepo := tests.CreateMockPluginRepo() mockPluginRepo := tests.CreateMockPluginRepo()
-1
View File
@@ -53,7 +53,6 @@ var _ = Describe("SchedulerService", Ordered, func() {
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Create mock scheduler and timer registry // Create mock scheduler and timer registry
mockSched = newMockScheduler() mockSched = newMockScheduler()
-1
View File
@@ -46,7 +46,6 @@ var _ = Describe("SubsonicAPI Host Function", Ordered, func() {
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Setup mock router and data store // Setup mock router and data store
router = &fakeSubsonicRouter{} router = &fakeSubsonicRouter{}
-1
View File
@@ -486,7 +486,6 @@ func setupTestUsersConfig(tmpDir string) {
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
} }
// testUsersInput represents input for test-users plugin calls // testUsersInput represents input for test-users plugin calls
+16 -36
View File
@@ -14,7 +14,6 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/navidrome/navidrome/conf" "github.com/navidrome/navidrome/conf"
@@ -54,7 +53,6 @@ var _ = Describe("WebSocketService", Ordered, func() {
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Setup mock DataStore with pre-enabled plugin // Setup mock DataStore with pre-enabled plugin
mockPluginRepo := tests.CreateMockPluginRepo() mockPluginRepo := tests.CreateMockPluginRepo()
@@ -295,10 +293,12 @@ var _ = Describe("WebSocketService", Ordered, func() {
Describe("Plugin Callbacks", func() { Describe("Plugin Callbacks", func() {
var wsServer *httptest.Server var wsServer *httptest.Server
var serverConn *websocket.Conn var serverConn *websocket.Conn
var serverMessages []string
var serverMu sync.Mutex var serverMu sync.Mutex
BeforeEach(func() { BeforeEach(func() {
serverConn = nil serverConn = nil
serverMessages = nil
upgrader := websocket.Upgrader{ upgrader := websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, CheckOrigin: func(r *http.Request) bool { return true },
@@ -312,12 +312,15 @@ var _ = Describe("WebSocketService", Ordered, func() {
serverConn = conn serverConn = conn
serverMu.Unlock() serverMu.Unlock()
// Keep connection open // Read and store messages
for { for {
_, _, err := conn.ReadMessage() _, msg, err := conn.ReadMessage()
if err != nil { if err != nil {
break break
} }
serverMu.Lock()
serverMessages = append(serverMessages, string(msg))
serverMu.Unlock()
} }
})) }))
@@ -336,36 +339,10 @@ var _ = Describe("WebSocketService", Ordered, func() {
} }
}) })
It("should invoke OnTextMessage callback when receiving text", func() {
ctx := GinkgoT().Context()
wsURL := "ws://" + strings.TrimPrefix(wsServer.URL, "http://")
connID, err := testService.Connect(ctx, wsURL, nil, "text-cb-conn")
Expect(err).ToNot(HaveOccurred())
// Wait for server to have the connection
Eventually(func() *websocket.Conn {
serverMu.Lock()
defer serverMu.Unlock()
return serverConn
}).ShouldNot(BeNil())
// Send message from server to plugin
serverMu.Lock()
err = serverConn.WriteMessage(websocket.TextMessage, []byte("test message"))
serverMu.Unlock()
Expect(err).ToNot(HaveOccurred())
// The plugin should have received the callback
// We can verify by checking the plugin's stored messages via vars
// For now we just verify no errors occurred
time.Sleep(100 * time.Millisecond)
_ = connID
})
It("should invoke OnBinaryMessage callback when receiving binary", func() { It("should invoke OnBinaryMessage callback when receiving binary", func() {
ctx := GinkgoT().Context() ctx := GinkgoT().Context()
wsURL := "ws://" + strings.TrimPrefix(wsServer.URL, "http://") wsURL := "ws://" + strings.TrimPrefix(wsServer.URL, "http://")
connID, err := testService.Connect(ctx, wsURL, nil, "binary-cb-conn") _, err := testService.Connect(ctx, wsURL, nil, "binary-cb-conn")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
// Wait for server to have the connection // Wait for server to have the connection
@@ -382,9 +359,13 @@ var _ = Describe("WebSocketService", Ordered, func() {
serverMu.Unlock() serverMu.Unlock()
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
// Give time for callback to execute // Plugin echoes binary data back as text prefixed with "binary_echo:"
time.Sleep(100 * time.Millisecond) expectedEcho := "binary_echo:" + base64.StdEncoding.EncodeToString(binaryData)
_ = connID Eventually(func() []string {
serverMu.Lock()
defer serverMu.Unlock()
return serverMessages
}).Should(ContainElement(expectedEcho))
}) })
It("should invoke OnClose callback when server closes connection", func() { It("should invoke OnClose callback when server closes connection", func() {
@@ -466,7 +447,7 @@ var _ = Describe("WebSocketService", Ordered, func() {
It("should allow plugin to send messages via host function", func() { It("should allow plugin to send messages via host function", func() {
ctx := GinkgoT().Context() ctx := GinkgoT().Context()
wsURL := "ws://" + strings.TrimPrefix(wsServer.URL, "http://") wsURL := "ws://" + strings.TrimPrefix(wsServer.URL, "http://")
connID, err := testService.Connect(ctx, wsURL, nil, "host-send-conn") _, err := testService.Connect(ctx, wsURL, nil, "host-send-conn")
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
// Wait for server to have the connection // Wait for server to have the connection
@@ -488,7 +469,6 @@ var _ = Describe("WebSocketService", Ordered, func() {
defer serverMu.Unlock() defer serverMu.Unlock()
return serverMessages return serverMessages
}).Should(ContainElement("echo:echo")) }).Should(ContainElement("echo:echo"))
_ = connID
}) })
It("should allow plugin to close connection via host function", func() { It("should allow plugin to close connection via host function", func() {
+26 -8
View File
@@ -81,9 +81,14 @@ var _ = Describe("callPluginFunction metrics", Ordered, func() {
Expect(calls[0].elapsed).To(BeNumerically(">=", 0)) Expect(calls[0].elapsed).To(BeNumerically(">=", 0))
}) })
It("records metrics for failed plugin calls (error returned)", func() { Context("with error config", Ordered, func() {
// Create a manager with error config to force plugin errors var (
errorRecorder := &mockMetricsRecorder{} errorRecorder *mockMetricsRecorder
errorAgent agents.Interface
)
BeforeAll(func() {
errorRecorder = &mockMetricsRecorder{}
errorManager, _ := createTestManagerWithPluginsAndMetrics( errorManager, _ := createTestManagerWithPluginsAndMetrics(
map[string]map[string]string{ map[string]map[string]string{
"test-metadata-agent": {"error": "simulated error"}, "test-metadata-agent": {"error": "simulated error"},
@@ -92,9 +97,12 @@ var _ = Describe("callPluginFunction metrics", Ordered, func() {
"test-metadata-agent"+PackageExtension, "test-metadata-agent"+PackageExtension,
) )
errorAgent, ok := errorManager.LoadMediaAgent("test-metadata-agent") var ok bool
errorAgent, ok = errorManager.LoadMediaAgent("test-metadata-agent")
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
})
It("records metrics for failed plugin calls (error returned)", func() {
retriever := errorAgent.(agents.ArtistBiographyRetriever) retriever := errorAgent.(agents.ArtistBiographyRetriever)
_, err := retriever.GetArtistBiography(GinkgoT().Context(), "artist-1", "Test Artist", "mbid") _, err := retriever.GetArtistBiography(GinkgoT().Context(), "artist-1", "Test Artist", "mbid")
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
@@ -105,19 +113,28 @@ var _ = Describe("callPluginFunction metrics", Ordered, func() {
Expect(calls[0].method).To(Equal(FuncGetArtistBiography)) Expect(calls[0].method).To(Equal(FuncGetArtistBiography))
Expect(calls[0].ok).To(BeFalse()) Expect(calls[0].ok).To(BeFalse())
}) })
})
It("does not record metrics for not-implemented functions", func() { Context("with partial metadata agent", Ordered, func() {
// Use partial metadata agent that doesn't implement GetArtistMBID var (
partialRecorder := &mockMetricsRecorder{} partialRecorder *mockMetricsRecorder
partialAgent agents.Interface
)
BeforeAll(func() {
partialRecorder = &mockMetricsRecorder{}
partialManager, _ := createTestManagerWithPluginsAndMetrics( partialManager, _ := createTestManagerWithPluginsAndMetrics(
nil, nil,
partialRecorder, partialRecorder,
"partial-metadata-agent"+PackageExtension, "partial-metadata-agent"+PackageExtension,
) )
partialAgent, ok := partialManager.LoadMediaAgent("partial-metadata-agent") var ok bool
partialAgent, ok = partialManager.LoadMediaAgent("partial-metadata-agent")
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
})
It("does not record metrics for not-implemented functions", func() {
retriever := partialAgent.(agents.ArtistMBIDRetriever) retriever := partialAgent.(agents.ArtistMBIDRetriever)
_, err := retriever.GetArtistMBID(GinkgoT().Context(), "artist-1", "Test Artist") _, err := retriever.GetArtistMBID(GinkgoT().Context(), "artist-1", "Test Artist")
Expect(err).To(MatchError(errNotImplemented)) Expect(err).To(MatchError(errNotImplemented))
@@ -126,3 +143,4 @@ var _ = Describe("callPluginFunction metrics", Ordered, func() {
Expect(calls).To(HaveLen(0)) Expect(calls).To(HaveLen(0))
}) })
}) })
})
+14 -1
View File
@@ -36,6 +36,20 @@ var (
func TestPlugins(t *testing.T) { func TestPlugins(t *testing.T) {
tests.Init(t, false) tests.Init(t, false)
buildTestPlugins(t, testDataDir) buildTestPlugins(t, testDataDir)
// Create a shared wazero compilation cache directory.
// All test managers will point CacheFolder here so that WASM compilation
// is done once per binary and then reused from disk cache.
sharedCacheDir, err := os.MkdirTemp("", "plugins-shared-cache-*")
if err != nil {
t.Fatalf("Failed to create shared cache dir: %v", err)
}
t.Cleanup(func() { os.RemoveAll(sharedCacheDir) })
// Set CacheFolder globally so all tests (including those using
// configtest.SetupConfig) inherit it without needing to set it manually.
conf.Server.CacheFolder = sharedCacheDir
log.SetLevel(log.LevelFatal) log.SetLevel(log.LevelFatal)
RegisterFailHandler(Fail) RegisterFailHandler(Fail)
RunSpecs(t, "Plugins Suite") RunSpecs(t, "Plugins Suite")
@@ -114,7 +128,6 @@ func createTestManagerWithPluginsAndMetrics(pluginConfig map[string]map[string]s
conf.Server.Plugins.Enabled = true conf.Server.Plugins.Enabled = true
conf.Server.Plugins.Folder = tmpDir conf.Server.Plugins.Folder = tmpDir
conf.Server.Plugins.AutoReload = false conf.Server.Plugins.AutoReload = false
conf.Server.CacheFolder = filepath.Join(tmpDir, "cache")
// Setup mock DataStore with pre-enabled plugins // Setup mock DataStore with pre-enabled plugins
mockPluginRepo := tests.CreateMockPluginRepo() mockPluginRepo := tests.CreateMockPluginRepo()
+44 -16
View File
@@ -58,18 +58,25 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() {
Expect(result).To(BeTrue()) Expect(result).To(BeTrue())
}) })
It("returns false when plugin is configured to not authorize", func() { Context("when plugin is configured to not authorize", Ordered, func() {
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{ var notAuthScrobbler scrobbler.Scrobbler
BeforeAll(func() {
mgr, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-scrobbler": {"authorized": "false"}, "test-scrobbler": {"authorized": "false"},
}, "test-scrobbler"+PackageExtension) }, "test-scrobbler"+PackageExtension)
sc, ok := manager.LoadScrobbler("test-scrobbler") var ok bool
notAuthScrobbler, ok = mgr.LoadScrobbler("test-scrobbler")
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
})
result := sc.IsAuthorized(ctxWithUser(), "user-1") It("returns false", func() {
result := notAuthScrobbler.IsAuthorized(ctxWithUser(), "user-1")
Expect(result).To(BeFalse()) Expect(result).To(BeFalse())
}) })
}) })
})
Describe("isUserAllowed", func() { Describe("isUserAllowed", func() {
It("returns true when allUsers is true", func() { It("returns true when allUsers is true", func() {
@@ -127,20 +134,27 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
It("returns error when plugin returns error", func() { Context("when plugin returns error", Ordered, func() {
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{ var retryScrobbler scrobbler.Scrobbler
BeforeAll(func() {
mgr, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-scrobbler": {"error": "service unavailable", "error_type": "scrobbler(retry_later)"}, "test-scrobbler": {"error": "service unavailable", "error_type": "scrobbler(retry_later)"},
}, "test-scrobbler"+PackageExtension) }, "test-scrobbler"+PackageExtension)
sc, ok := manager.LoadScrobbler("test-scrobbler") var ok bool
retryScrobbler, ok = mgr.LoadScrobbler("test-scrobbler")
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
})
It("returns ErrRetryLater", func() {
track := &model.MediaFile{ID: "track-1", Title: "Test Song"} track := &model.MediaFile{ID: "track-1", Title: "Test Song"}
err := sc.NowPlaying(ctxWithUser(), "user-1", track, 30) err := retryScrobbler.NowPlaying(ctxWithUser(), "user-1", track, 30)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(scrobbler.ErrRetryLater)) Expect(err).To(MatchError(scrobbler.ErrRetryLater))
}) })
}) })
})
Describe("Scrobble", func() { Describe("Scrobble", func() {
It("successfully calls the plugin", func() { It("successfully calls the plugin", func() {
@@ -166,40 +180,54 @@ var _ = Describe("ScrobblerPlugin", Ordered, func() {
Expect(err).ToNot(HaveOccurred()) Expect(err).ToNot(HaveOccurred())
}) })
It("returns error when plugin returns not_authorized error", func() { Context("when plugin returns not_authorized error", Ordered, func() {
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{ var notAuthScrobbler scrobbler.Scrobbler
BeforeAll(func() {
mgr, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-scrobbler": {"error": "user not linked", "error_type": "scrobbler(not_authorized)"}, "test-scrobbler": {"error": "user not linked", "error_type": "scrobbler(not_authorized)"},
}, "test-scrobbler"+PackageExtension) }, "test-scrobbler"+PackageExtension)
sc, ok := manager.LoadScrobbler("test-scrobbler") var ok bool
notAuthScrobbler, ok = mgr.LoadScrobbler("test-scrobbler")
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
})
It("returns ErrNotAuthorized", func() {
scrobble := scrobbler.Scrobble{ scrobble := scrobbler.Scrobble{
MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"}, MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"},
TimeStamp: time.Now(), TimeStamp: time.Now(),
} }
err := sc.Scrobble(ctxWithUser(), "user-1", scrobble) err := notAuthScrobbler.Scrobble(ctxWithUser(), "user-1", scrobble)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(scrobbler.ErrNotAuthorized)) Expect(err).To(MatchError(scrobbler.ErrNotAuthorized))
}) })
})
It("returns error when plugin returns unrecoverable error", func() { Context("when plugin returns unrecoverable error", Ordered, func() {
manager, _ := createTestManagerWithPlugins(map[string]map[string]string{ var unrecoverableScrobbler scrobbler.Scrobbler
BeforeAll(func() {
mgr, _ := createTestManagerWithPlugins(map[string]map[string]string{
"test-scrobbler": {"error": "track rejected", "error_type": "scrobbler(unrecoverable)"}, "test-scrobbler": {"error": "track rejected", "error_type": "scrobbler(unrecoverable)"},
}, "test-scrobbler"+PackageExtension) }, "test-scrobbler"+PackageExtension)
sc, ok := manager.LoadScrobbler("test-scrobbler") var ok bool
unrecoverableScrobbler, ok = mgr.LoadScrobbler("test-scrobbler")
Expect(ok).To(BeTrue()) Expect(ok).To(BeTrue())
})
It("returns ErrUnrecoverable", func() {
scrobble := scrobbler.Scrobble{ scrobble := scrobbler.Scrobble{
MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"}, MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"},
TimeStamp: time.Now(), TimeStamp: time.Now(),
} }
err := sc.Scrobble(ctxWithUser(), "user-1", scrobble) err := unrecoverableScrobbler.Scrobble(ctxWithUser(), "user-1", scrobble)
Expect(err).To(HaveOccurred()) Expect(err).To(HaveOccurred())
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable)) Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
}) })
}) })
})
Describe("PluginNames", func() { Describe("PluginNames", func() {
It("returns plugin names with Scrobbler capability", func() { It("returns plugin names with Scrobbler capability", func() {
+3 -2
View File
@@ -45,10 +45,11 @@ func (t *testWebSocket) OnTextMessage(input websocket.OnTextMessageRequest) erro
} }
// OnBinaryMessage is called when a binary message is received. // OnBinaryMessage is called when a binary message is received.
// Echoes the data back as a text message prefixed with "binary_echo:" so tests
// can observe the callback fired.
func (t *testWebSocket) OnBinaryMessage(input websocket.OnBinaryMessageRequest) error { func (t *testWebSocket) OnBinaryMessage(input websocket.OnBinaryMessageRequest) error {
// Store received binary data for test verification
storeReceivedMessage("binary:" + input.Data) storeReceivedMessage("binary:" + input.Data)
return nil return host.WebSocketSendText(input.ConnectionID, "binary_echo:"+input.Data)
} }
// OnError is called when an error occurs on a WebSocket connection. // OnError is called when an error occurs on a WebSocket connection.