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:
@@ -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{}}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
Operation: "get",
|
||||||
output, err = callTestKVStore(ctx, testKVStoreInput{
|
Key: "ttl_key",
|
||||||
Operation: "get",
|
})
|
||||||
Key: "ttl_key",
|
g.Expect(err).ToNot(HaveOccurred())
|
||||||
})
|
g.Expect(output.Exists).To(BeFalse())
|
||||||
Expect(err).ToNot(HaveOccurred())
|
}).WithTimeout(3 * time.Second).WithPolling(200 * time.Millisecond).Should(Succeed())
|
||||||
Expect(output.Exists).To(BeFalse())
|
|
||||||
})
|
})
|
||||||
|
|
||||||
It("should delete keys by prefix", func() {
|
It("should delete keys by prefix", func() {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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{}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -81,48 +81,66 @@ 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
|
||||||
errorManager, _ := createTestManagerWithPluginsAndMetrics(
|
errorAgent agents.Interface
|
||||||
map[string]map[string]string{
|
|
||||||
"test-metadata-agent": {"error": "simulated error"},
|
|
||||||
},
|
|
||||||
errorRecorder,
|
|
||||||
"test-metadata-agent"+PackageExtension,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
errorAgent, ok := errorManager.LoadMediaAgent("test-metadata-agent")
|
BeforeAll(func() {
|
||||||
Expect(ok).To(BeTrue())
|
errorRecorder = &mockMetricsRecorder{}
|
||||||
|
errorManager, _ := createTestManagerWithPluginsAndMetrics(
|
||||||
|
map[string]map[string]string{
|
||||||
|
"test-metadata-agent": {"error": "simulated error"},
|
||||||
|
},
|
||||||
|
errorRecorder,
|
||||||
|
"test-metadata-agent"+PackageExtension,
|
||||||
|
)
|
||||||
|
|
||||||
retriever := errorAgent.(agents.ArtistBiographyRetriever)
|
var ok bool
|
||||||
_, err := retriever.GetArtistBiography(GinkgoT().Context(), "artist-1", "Test Artist", "mbid")
|
errorAgent, ok = errorManager.LoadMediaAgent("test-metadata-agent")
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(ok).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
calls := errorRecorder.getCalls()
|
It("records metrics for failed plugin calls (error returned)", func() {
|
||||||
Expect(calls).To(HaveLen(1))
|
retriever := errorAgent.(agents.ArtistBiographyRetriever)
|
||||||
Expect(calls[0].plugin).To(Equal("test-metadata-agent"))
|
_, err := retriever.GetArtistBiography(GinkgoT().Context(), "artist-1", "Test Artist", "mbid")
|
||||||
Expect(calls[0].method).To(Equal(FuncGetArtistBiography))
|
Expect(err).To(HaveOccurred())
|
||||||
Expect(calls[0].ok).To(BeFalse())
|
|
||||||
|
calls := errorRecorder.getCalls()
|
||||||
|
Expect(calls).To(HaveLen(1))
|
||||||
|
Expect(calls[0].plugin).To(Equal("test-metadata-agent"))
|
||||||
|
Expect(calls[0].method).To(Equal(FuncGetArtistBiography))
|
||||||
|
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
|
||||||
partialManager, _ := createTestManagerWithPluginsAndMetrics(
|
partialAgent agents.Interface
|
||||||
nil,
|
|
||||||
partialRecorder,
|
|
||||||
"partial-metadata-agent"+PackageExtension,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
partialAgent, ok := partialManager.LoadMediaAgent("partial-metadata-agent")
|
BeforeAll(func() {
|
||||||
Expect(ok).To(BeTrue())
|
partialRecorder = &mockMetricsRecorder{}
|
||||||
|
partialManager, _ := createTestManagerWithPluginsAndMetrics(
|
||||||
|
nil,
|
||||||
|
partialRecorder,
|
||||||
|
"partial-metadata-agent"+PackageExtension,
|
||||||
|
)
|
||||||
|
|
||||||
retriever := partialAgent.(agents.ArtistMBIDRetriever)
|
var ok bool
|
||||||
_, err := retriever.GetArtistMBID(GinkgoT().Context(), "artist-1", "Test Artist")
|
partialAgent, ok = partialManager.LoadMediaAgent("partial-metadata-agent")
|
||||||
Expect(err).To(MatchError(errNotImplemented))
|
Expect(ok).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
calls := partialRecorder.getCalls()
|
It("does not record metrics for not-implemented functions", func() {
|
||||||
Expect(calls).To(HaveLen(0))
|
retriever := partialAgent.(agents.ArtistMBIDRetriever)
|
||||||
|
_, err := retriever.GetArtistMBID(GinkgoT().Context(), "artist-1", "Test Artist")
|
||||||
|
Expect(err).To(MatchError(errNotImplemented))
|
||||||
|
|
||||||
|
calls := partialRecorder.getCalls()
|
||||||
|
Expect(calls).To(HaveLen(0))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -58,16 +58,23 @@ 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
|
||||||
"test-scrobbler": {"authorized": "false"},
|
|
||||||
}, "test-scrobbler"+PackageExtension)
|
|
||||||
|
|
||||||
sc, ok := manager.LoadScrobbler("test-scrobbler")
|
BeforeAll(func() {
|
||||||
Expect(ok).To(BeTrue())
|
mgr, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
||||||
|
"test-scrobbler": {"authorized": "false"},
|
||||||
|
}, "test-scrobbler"+PackageExtension)
|
||||||
|
|
||||||
result := sc.IsAuthorized(ctxWithUser(), "user-1")
|
var ok bool
|
||||||
Expect(result).To(BeFalse())
|
notAuthScrobbler, ok = mgr.LoadScrobbler("test-scrobbler")
|
||||||
|
Expect(ok).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("returns false", func() {
|
||||||
|
result := notAuthScrobbler.IsAuthorized(ctxWithUser(), "user-1")
|
||||||
|
Expect(result).To(BeFalse())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -127,18 +134,25 @@ 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
|
||||||
"test-scrobbler": {"error": "service unavailable", "error_type": "scrobbler(retry_later)"},
|
|
||||||
}, "test-scrobbler"+PackageExtension)
|
|
||||||
|
|
||||||
sc, ok := manager.LoadScrobbler("test-scrobbler")
|
BeforeAll(func() {
|
||||||
Expect(ok).To(BeTrue())
|
mgr, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
||||||
|
"test-scrobbler": {"error": "service unavailable", "error_type": "scrobbler(retry_later)"},
|
||||||
|
}, "test-scrobbler"+PackageExtension)
|
||||||
|
|
||||||
track := &model.MediaFile{ID: "track-1", Title: "Test Song"}
|
var ok bool
|
||||||
err := sc.NowPlaying(ctxWithUser(), "user-1", track, 30)
|
retryScrobbler, ok = mgr.LoadScrobbler("test-scrobbler")
|
||||||
Expect(err).To(HaveOccurred())
|
Expect(ok).To(BeTrue())
|
||||||
Expect(err).To(MatchError(scrobbler.ErrRetryLater))
|
})
|
||||||
|
|
||||||
|
It("returns ErrRetryLater", func() {
|
||||||
|
track := &model.MediaFile{ID: "track-1", Title: "Test Song"}
|
||||||
|
err := retryScrobbler.NowPlaying(ctxWithUser(), "user-1", track, 30)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err).To(MatchError(scrobbler.ErrRetryLater))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -166,38 +180,52 @@ 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
|
||||||
"test-scrobbler": {"error": "user not linked", "error_type": "scrobbler(not_authorized)"},
|
|
||||||
}, "test-scrobbler"+PackageExtension)
|
|
||||||
|
|
||||||
sc, ok := manager.LoadScrobbler("test-scrobbler")
|
BeforeAll(func() {
|
||||||
Expect(ok).To(BeTrue())
|
mgr, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
||||||
|
"test-scrobbler": {"error": "user not linked", "error_type": "scrobbler(not_authorized)"},
|
||||||
|
}, "test-scrobbler"+PackageExtension)
|
||||||
|
|
||||||
scrobble := scrobbler.Scrobble{
|
var ok bool
|
||||||
MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"},
|
notAuthScrobbler, ok = mgr.LoadScrobbler("test-scrobbler")
|
||||||
TimeStamp: time.Now(),
|
Expect(ok).To(BeTrue())
|
||||||
}
|
})
|
||||||
err := sc.Scrobble(ctxWithUser(), "user-1", scrobble)
|
|
||||||
Expect(err).To(HaveOccurred())
|
It("returns ErrNotAuthorized", func() {
|
||||||
Expect(err).To(MatchError(scrobbler.ErrNotAuthorized))
|
scrobble := scrobbler.Scrobble{
|
||||||
|
MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"},
|
||||||
|
TimeStamp: time.Now(),
|
||||||
|
}
|
||||||
|
err := notAuthScrobbler.Scrobble(ctxWithUser(), "user-1", scrobble)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
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
|
||||||
"test-scrobbler": {"error": "track rejected", "error_type": "scrobbler(unrecoverable)"},
|
|
||||||
}, "test-scrobbler"+PackageExtension)
|
|
||||||
|
|
||||||
sc, ok := manager.LoadScrobbler("test-scrobbler")
|
BeforeAll(func() {
|
||||||
Expect(ok).To(BeTrue())
|
mgr, _ := createTestManagerWithPlugins(map[string]map[string]string{
|
||||||
|
"test-scrobbler": {"error": "track rejected", "error_type": "scrobbler(unrecoverable)"},
|
||||||
|
}, "test-scrobbler"+PackageExtension)
|
||||||
|
|
||||||
scrobble := scrobbler.Scrobble{
|
var ok bool
|
||||||
MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"},
|
unrecoverableScrobbler, ok = mgr.LoadScrobbler("test-scrobbler")
|
||||||
TimeStamp: time.Now(),
|
Expect(ok).To(BeTrue())
|
||||||
}
|
})
|
||||||
err := sc.Scrobble(ctxWithUser(), "user-1", scrobble)
|
|
||||||
Expect(err).To(HaveOccurred())
|
It("returns ErrUnrecoverable", func() {
|
||||||
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
|
scrobble := scrobbler.Scrobble{
|
||||||
|
MediaFile: model.MediaFile{ID: "track-1", Title: "Test Song"},
|
||||||
|
TimeStamp: time.Now(),
|
||||||
|
}
|
||||||
|
err := unrecoverableScrobbler.Scrobble(ctxWithUser(), "user-1", scrobble)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err).To(MatchError(scrobbler.ErrUnrecoverable))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user