From d660bbd90050ce69397eab37535ff083b7c9298d Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 3 Jan 2026 16:31:00 -0500 Subject: [PATCH] test: add integration tests for UsersService enable gate behavior Signed-off-by: Deluan --- plugins/README.md | 82 ++++++++++++++++++++++++++++++++++++++ plugins/host_users_test.go | 72 +++++++++++++++++++++++++++++++-- 2 files changed, 151 insertions(+), 3 deletions(-) diff --git a/plugins/README.md b/plugins/README.md index 082322e01..1c03fa238 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -30,6 +30,7 @@ The plugin system is built on **[Extism](https://extism.org/)**, a cross-languag - [Artwork](#artwork) - [SubsonicAPI](#subsonicapi) - [Config](#config) + - [Users](#users) - [Configuration](#configuration) - [Building Plugins](#building-plugins) - [Examples](#examples) @@ -708,6 +709,86 @@ for _, key := range keys { allKeys := host.ConfigKeys("") ``` +### Users + +Access user information for the users that the plugin has been granted access to. This is useful for plugins that need to associate data with specific users or display user information. + +**Manifest permission:** + +```json +{ + "permissions": { + "users": { + "reason": "Display user information in status updates" + } + } +} +``` + +**Important:** Before enabling a plugin that requires the `users` permission, an administrator must configure which users the plugin can access. This can be done in two ways: + +1. **Allow all users** – Enable the "Allow all users" toggle in the plugin settings +2. **Select specific users** – Choose individual users from the user list + +If neither option is configured, the plugin cannot be enabled. + +**Host functions:** + +| Function | Parameters | Returns | +|------------------|------------|-----------------------| +| `users_getusers` | – | Array of User objects | + +**User object fields:** + +| Field | Type | Description | +|------------|---------|--------------------------------| +| `userName` | string | The user's unique username | +| `name` | string | The user's display name | +| `isAdmin` | boolean | Whether the user is an admin | + +> **Security:** Sensitive fields like passwords, email addresses, and internal IDs are never exposed to plugins. + +**Usage (with generated SDK):** + +```go +import "github.com/navidrome/navidrome/plugins/pdk/go/host" + +// Get all users the plugin has access to +users, err := host.UsersGetUsers() +if err != nil { + pdk.Log(pdk.LogError, "Failed to get users: " + err.Error()) + return +} + +for _, user := range users { + pdk.Log(pdk.LogInfo, "User: " + user.UserName + " (" + user.Name + ")") + if user.IsAdmin { + pdk.Log(pdk.LogInfo, " - Administrator") + } +} +``` + +**Rust example:** + +```rust +use nd_pdk_host::users::get_users; + +let users = get_users()?; +for user in users { + println!("User: {} ({})", user.user_name, user.name); +} +``` + +**Python example:** + +```python +from host.nd_host_users import users_get_users + +users = users_get_users() +for user in users: + print(f"User: {user['userName']} ({user['name']})") +``` + --- ## Configuration @@ -959,6 +1040,7 @@ Plugins run in a secure WebAssembly sandbox provided by [Extism](https://extism. 4. **Config Isolation** – Plugins only receive their own config section 5. **Memory Limits** – Controlled by the WebAssembly runtime 6. **SubsonicAPI Restrictions** – Configurable user/admin access controls +7. **Users Permission** – Plugins requesting user access must be explicitly configured with allowed users; sensitive data (passwords, emails) is never exposed --- diff --git a/plugins/host_users_test.go b/plugins/host_users_test.go index b46e94fdb..0ca62fff5 100644 --- a/plugins/host_users_test.go +++ b/plugins/host_users_test.go @@ -243,6 +243,66 @@ var _ = Describe("UsersService Integration with Specific Users", Ordered, func() }) }) +var _ = Describe("UsersService Enable Gate", Ordered, func() { + var manager *Manager + + BeforeAll(func() { + var cleanup func() + // Start with disabled plugin, no users configured + manager, cleanup = setupUsersIntegrationManagerWithEnabled(false, false, "") + DeferCleanup(cleanup) + }) + + Describe("Enable Gate Behavior", func() { + It("should block enabling when no users configured and allUsers is false", func() { + ctx := GinkgoT().Context() + err := manager.EnablePlugin(ctx, "test-users") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("users permission requires configuration")) + }) + + It("should allow enabling when allUsers is true", func() { + ctx := GinkgoT().Context() + + // Update the plugin to have allUsers=true + err := manager.UpdatePluginUsers(ctx, "test-users", "", true) + Expect(err).ToNot(HaveOccurred()) + + // Now enabling should succeed + err = manager.EnablePlugin(ctx, "test-users") + Expect(err).ToNot(HaveOccurred()) + + // Verify plugin is loaded + manager.mu.RLock() + _, ok := manager.plugins["test-users"] + manager.mu.RUnlock() + Expect(ok).To(BeTrue()) + }) + + It("should allow enabling when specific users are configured", func() { + ctx := GinkgoT().Context() + + // First disable the plugin + err := manager.DisablePlugin(ctx, "test-users") + Expect(err).ToNot(HaveOccurred()) + + // Update to have specific users (and allUsers=false) + err = manager.UpdatePluginUsers(ctx, "test-users", `["user1"]`, false) + Expect(err).ToNot(HaveOccurred()) + + // Now enabling should succeed + err = manager.EnablePlugin(ctx, "test-users") + Expect(err).ToNot(HaveOccurred()) + + // Verify plugin is loaded + manager.mu.RLock() + _, ok := manager.plugins["test-users"] + manager.mu.RUnlock() + Expect(ok).To(BeTrue()) + }) + }) +}) + // testUsersSetup contains common setup data for users integration tests type testUsersSetup struct { tmpDir string @@ -357,8 +417,14 @@ func callTestUsersPlugin(ctx context.Context, manager *Manager, input testUsersI return &output, nil } -// setupUsersIntegrationManager creates a Manager for users integration tests with the given plugin settings +// setupUsersIntegrationManager creates a Manager for users integration tests with the given plugin settings. +// The plugin is enabled by default. func setupUsersIntegrationManager(allUsers bool, allowedUsers string) (*Manager, func()) { + return setupUsersIntegrationManagerWithEnabled(true, allUsers, allowedUsers) +} + +// setupUsersIntegrationManagerWithEnabled creates a Manager for users integration tests with full control over plugin state +func setupUsersIntegrationManagerWithEnabled(enabled, allUsers bool, allowedUsers string) (*Manager, func()) { setup, err := setupTestUsersPlugin() Expect(err).ToNot(HaveOccurred()) @@ -366,14 +432,14 @@ func setupUsersIntegrationManager(allUsers bool, allowedUsers string) (*Manager, cleanupConfig := configtest.SetupConfig() setupTestUsersConfig(setup.tmpDir) - // Setup mock DataStore with pre-enabled plugin and users + // Setup mock DataStore with plugin and users mockPluginRepo := tests.CreateMockPluginRepo() mockPluginRepo.Permitted = true mockPluginRepo.SetData(model.Plugins{{ ID: "test-users", Path: setup.destPath, SHA256: setup.hashHex, - Enabled: true, + Enabled: enabled, AllUsers: allUsers, Users: allowedUsers, }})