test: add integration tests for UsersService enable gate behavior

Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
Deluan 2026-01-03 16:31:00 -05:00
parent 2d4a3c621c
commit d660bbd900
2 changed files with 151 additions and 3 deletions

View File

@ -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
---

View File

@ -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,
}})