mirror of
https://github.com/navidrome/navidrome.git
synced 2026-07-09 17:18:45 +00:00
Some checks are pending
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
* feat(plugins): export ReadPackageManifest and ValidatePackage for off-disk inspection * test(plugins): cover ValidatePackage cross-field validation branch * feat(cmd): add 'plugin list' command * refactor(cmd): drop premature pluginManager interface until first use * feat(cmd): add 'plugin enable' and 'plugin disable' commands * feat(cmd): add 'plugin edit' for config and permission updates * test(cmd): cover plugin edit aborting on config validation failure * feat(cmd): add 'plugin info' and 'plugin validate' (installed id or .ndp file) * test(cmd): unit-test formatManifestInfo nil-guard and json branches * feat(cmd): add 'plugin rescan' command * feat(cmd): enrich plugin info text output and re-validate config on validate * fix(cmd): omit zero-value timestamps in plugin info text output * fix(cmd): give plugin list and info separate format flag vars A shared package-global bound to both commands' --format flag caused the later init() registration (info, default "text") to clobber the earlier one (list, default "table"), so 'plugin list' with no -f failed with 'invalid output format "text"'. Split into pluginListFormat / pluginInfoFormat and add a regression test on the registered defaults. * fix(plugins): address code-review findings on plugin CLI - edit: read-modify-write merge so flipping one permission flag no longer wipes unspecified fields (matches the native API); reject non-JSON --users/--libraries values. - info: return an error on unknown --format (was silently text); include sha256 in off-disk JSON output; align the text columns. - move declaredPermissions onto plugins.Permissions.DeclaredNames() as the single source of truth; drop ValidatePackage's redundant Validate call. - readConfigFile: pass ctx to log.Fatal; copy flag globals before taking their address; trim comments that restated the code. * refactor(plugins): export ReadManifest/ComputeFileSHA256, drop thin CLI wrappers Remove the ReadPackageManifest/ComputeFileSHA256/ValidatePackage wrappers in favor of exporting the underlying readManifest->ReadManifest and computeFileSHA256->ComputeFileSHA256 directly. ValidatePackage was identical to ReadPackageManifest (readManifest already validates via ParseManifest), so the CLI's validate path now calls ReadManifest too. * test(plugins): consolidate ReadManifest specs and move ComputeFileSHA256 tests Merge the two ReadManifest Describe blocks into one, and move the ComputeFileSHA256 tests out of package_test.go into a new manager_sync_test.go (where the function now lives), deduping the overlapping hash specs. * test(plugins): use createTestPackage everywhere, drop writeNDP helper The schema-invalid and cross-field ReadManifest specs now build packages with createTestPackage (typed Manifest) instead of the raw-JSON writeNDP helper, so there is a single package-building helper. Also trims the gratuitous 1MB wasm buffer in the read-only spec to a few bytes. * test(plugins): drop duplicate missing-manifest spec from ReadManifest The openPackage block already covers the missing-manifest.json error path (shared zip-walk code); ReadManifest's identical copy added no distinct coverage. * test(plugins): fix misleading ReadManifest spec name and drop unused wasm bytes The spec was named 'should read only the manifest without loading wasm' but ReadManifest returns only (*Manifest, error) — it cannot assert whether wasm was loaded. Rename to what it actually verifies (manifest parses from a package that also contains a wasm entry) and pass nil wasm bytes, since the contents are never observed. * fix(cmd): address PR review feedback on plugin CLI - edit --users/--libraries now accept both comma-separated and JSON-array input (CSV is converted to the JSON the manager stores); help text documents both. - Permissions.DeclaredNames() derives names by reflecting over the generated struct's json tags instead of a hand-maintained list, so new permission types are picked up automatically. - isPackagePath checks only the .ndp suffix, so a mistyped package path yields a precise 'no such file' error instead of falling back to a misleading 'Plugin not found'. - Restore a ReadManifest test for the missing-manifest.json error path (it has its own branch, separate from openPackage). * docs(cmd): trim verbose comments to the why * fix(cmd): setting an explicit users/libraries list clears the all-* flag Per Codex review: with allUsers/allLibraries true, 'plugin edit --users <list>' preserved the all-* flag so the allow-list was silently ignored, and the mutually-exclusive flags meant it couldn't be corrected in one command. An explicit list now implies all-*=false.
360 lines
13 KiB
Go
360 lines
13 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/plugins"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var samplePlugins = model.Plugins{
|
|
{ID: "alpha", Manifest: `{"name":"Alpha","version":"1.0.0","author":"me"}`, Enabled: true},
|
|
{ID: "beta", Manifest: `{"name":"Beta","version":"2.1.0","author":"me"}`, Enabled: false, LastError: "boom"},
|
|
}
|
|
|
|
var _ = Describe("plugin command format flags", func() {
|
|
// Regression: list and info must not share a format variable. Binding the
|
|
// same var on both commands makes the last init() registration clobber the
|
|
// other's default, breaking `plugin list` with no -f flag.
|
|
It("defaults `list -f` to table", func() {
|
|
Expect(pluginListCmd.Flags().Lookup("format").DefValue).To(Equal("table"))
|
|
})
|
|
|
|
It("defaults `info -f` to text", func() {
|
|
Expect(pluginInfoCmd.Flags().Lookup("format").DefValue).To(Equal("text"))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("formatPluginList", func() {
|
|
It("renders csv with a header and one row per plugin", func() {
|
|
out, err := formatPluginList(samplePlugins, "csv")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
lines := strings.Split(strings.TrimSpace(out), "\n")
|
|
Expect(lines).To(HaveLen(3)) // header + 2 rows
|
|
Expect(lines[0]).To(ContainSubstring("id"))
|
|
Expect(out).To(ContainSubstring("alpha"))
|
|
Expect(out).To(ContainSubstring("beta"))
|
|
})
|
|
|
|
It("renders valid json", func() {
|
|
out, err := formatPluginList(samplePlugins, "json")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
var got []map[string]any
|
|
Expect(json.Unmarshal([]byte(out), &got)).To(Succeed())
|
|
Expect(got).To(HaveLen(2))
|
|
})
|
|
|
|
It("renders a human table by default", func() {
|
|
out, err := formatPluginList(samplePlugins, "table")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring("Alpha"))
|
|
Expect(out).To(ContainSubstring("1.0.0"))
|
|
})
|
|
|
|
It("errors on an unknown format", func() {
|
|
_, err := formatPluginList(samplePlugins, "yaml")
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
var _ = Describe("enable/disable plugin", func() {
|
|
It("calls EnablePlugin on the manager", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
err := enablePlugin(context.Background(), mgr, "alpha")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.EnablePluginCalls).To(Equal([]string{"alpha"}))
|
|
})
|
|
|
|
It("calls DisablePlugin on the manager", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
err := disablePlugin(context.Background(), mgr, "beta")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.DisablePluginCalls).To(Equal([]string{"beta"}))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("applyPluginEdit", func() {
|
|
var cur *model.Plugin
|
|
BeforeEach(func() {
|
|
cur = &model.Plugin{ID: "alpha", Users: `["bob"]`, Libraries: `[1,2]`, AllowWriteAccess: true}
|
|
})
|
|
|
|
It("validates then updates config when config is provided", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
cfg := `{"key":"val"}`
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{config: &cfg})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.ValidatePluginConfigCalls).To(HaveLen(1))
|
|
Expect(mgr.ValidatePluginConfigCalls[0].ConfigJSON).To(Equal(cfg))
|
|
Expect(mgr.UpdatePluginConfigCalls).To(HaveLen(1))
|
|
Expect(mgr.UpdatePluginConfigCalls[0].ConfigJSON).To(Equal(cfg))
|
|
})
|
|
|
|
It("updates users with allUsers flag", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
all := true
|
|
err := applyPluginEdit(context.Background(), mgr, cur,
|
|
pluginEditOptions{allUsers: &all})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.UpdatePluginUsersCalls).To(HaveLen(1))
|
|
Expect(mgr.UpdatePluginUsersCalls[0].AllUsers).To(BeTrue())
|
|
})
|
|
|
|
It("updates libraries with allLibraries and write access", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
all := true
|
|
wr := true
|
|
err := applyPluginEdit(context.Background(), mgr, cur,
|
|
pluginEditOptions{allLibraries: &all, writeAccess: &wr})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.UpdatePluginLibrariesCalls).To(HaveLen(1))
|
|
Expect(mgr.UpdatePluginLibrariesCalls[0].AllLibraries).To(BeTrue())
|
|
Expect(mgr.UpdatePluginLibrariesCalls[0].AllowWriteAccess).To(BeTrue())
|
|
})
|
|
|
|
It("preserves existing fields when only the write-access flag changes", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
no := false
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{writeAccess: &no})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.UpdatePluginLibrariesCalls).To(HaveLen(1))
|
|
Expect(mgr.UpdatePluginLibrariesCalls[0].LibrariesJSON).To(Equal(`[1,2]`)) // not wiped
|
|
Expect(mgr.UpdatePluginLibrariesCalls[0].AllowWriteAccess).To(BeFalse())
|
|
})
|
|
|
|
It("preserves existing users when only the all-users flag changes", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
all := true
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{allUsers: &all})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.UpdatePluginUsersCalls[0].UsersJSON).To(Equal(`["bob"]`)) // not wiped
|
|
})
|
|
|
|
It("parses a comma-separated users value into a JSON array", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
users := "alice, bob"
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{users: &users})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.UpdatePluginUsersCalls[0].UsersJSON).To(Equal(`["alice","bob"]`))
|
|
})
|
|
|
|
It("passes a JSON-array users value through unchanged", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
users := `["alice","bob"]`
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{users: &users})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.UpdatePluginUsersCalls[0].UsersJSON).To(Equal(`["alice","bob"]`))
|
|
})
|
|
|
|
It("rejects a malformed JSON-array users value", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
users := `["alice"`
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{users: &users})
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(mgr.UpdatePluginUsersCalls).To(BeEmpty())
|
|
})
|
|
|
|
It("parses a comma-separated libraries value into a JSON array of ints", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
libs := "1, 2"
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{libraries: &libs})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.UpdatePluginLibrariesCalls[0].LibrariesJSON).To(Equal(`[1,2]`))
|
|
})
|
|
|
|
It("rejects a non-integer library ID", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
libs := "1,abc"
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{libraries: &libs})
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(mgr.UpdatePluginLibrariesCalls).To(BeEmpty())
|
|
})
|
|
|
|
It("clears allUsers when an explicit users list is set", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
cur.AllUsers = true
|
|
users := "alice"
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{users: &users})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.UpdatePluginUsersCalls[0].UsersJSON).To(Equal(`["alice"]`))
|
|
Expect(mgr.UpdatePluginUsersCalls[0].AllUsers).To(BeFalse())
|
|
})
|
|
|
|
It("clears allLibraries when an explicit libraries list is set", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
cur.AllLibraries = true
|
|
libs := "1"
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{libraries: &libs})
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.UpdatePluginLibrariesCalls[0].LibrariesJSON).To(Equal(`[1]`))
|
|
Expect(mgr.UpdatePluginLibrariesCalls[0].AllLibraries).To(BeFalse())
|
|
})
|
|
|
|
It("does nothing and errors when no fields are set", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{})
|
|
Expect(err).To(HaveOccurred())
|
|
})
|
|
|
|
It("aborts the config update when validation fails", func() {
|
|
mgr := &tests.MockPluginManager{ValidateError: errors.New("bad config")}
|
|
cfg := `{"key":"val"}`
|
|
err := applyPluginEdit(context.Background(), mgr, cur, pluginEditOptions{config: &cfg})
|
|
Expect(err).To(HaveOccurred())
|
|
Expect(mgr.ValidatePluginConfigCalls).To(HaveLen(1))
|
|
Expect(mgr.UpdatePluginConfigCalls).To(BeEmpty())
|
|
})
|
|
})
|
|
|
|
var _ = Describe("isPackagePath", func() {
|
|
It("is true for any .ndp path", func() {
|
|
Expect(isPackagePath("/some/dir/x.ndp")).To(BeTrue())
|
|
})
|
|
It("is true for a non-existent .ndp path (so ReadManifest reports the error)", func() {
|
|
Expect(isPackagePath("/nope/x.ndp")).To(BeTrue())
|
|
})
|
|
It("is false for a bare plugin id", func() {
|
|
Expect(isPackagePath("my-plugin")).To(BeFalse())
|
|
})
|
|
})
|
|
|
|
var _ = Describe("formatPluginInfo", func() {
|
|
It("renders installed plugin details as text", func() {
|
|
p := &model.Plugin{ID: "alpha", Manifest: `{"name":"Alpha","version":"1.0.0","author":"me"}`, Enabled: true}
|
|
out, err := formatPluginInfo(p, "text")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring("alpha"))
|
|
Expect(out).To(ContainSubstring("Alpha"))
|
|
})
|
|
It("renders json", func() {
|
|
p := &model.Plugin{ID: "alpha", Manifest: `{}`}
|
|
out, err := formatPluginInfo(p, "json")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring("alpha"))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("formatManifestInfo", func() {
|
|
It("renders text with name, version, author", func() {
|
|
m := &plugins.Manifest{Name: "My Plugin", Version: "2.0.0", Author: "me"}
|
|
out, err := formatManifestInfo(m, "abc123", "text")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring("My Plugin"))
|
|
Expect(out).To(ContainSubstring("2.0.0"))
|
|
Expect(out).To(ContainSubstring("me"))
|
|
})
|
|
|
|
It("omits Description and Website when nil", func() {
|
|
m := &plugins.Manifest{Name: "X", Version: "1.0.0", Author: "a"}
|
|
out, err := formatManifestInfo(m, "abc123", "text")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).ToNot(ContainSubstring("Description:"))
|
|
Expect(out).ToNot(ContainSubstring("Website:"))
|
|
})
|
|
|
|
It("includes Description and Website when set", func() {
|
|
desc := "a cool plugin"
|
|
site := "https://example.com"
|
|
m := &plugins.Manifest{Name: "X", Version: "1.0.0", Author: "a", Description: &desc, Website: &site}
|
|
out, err := formatManifestInfo(m, "abc123", "text")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring("a cool plugin"))
|
|
Expect(out).To(ContainSubstring("https://example.com"))
|
|
})
|
|
|
|
It("renders valid json", func() {
|
|
m := &plugins.Manifest{Name: "X", Version: "1.0.0", Author: "a"}
|
|
out, err := formatManifestInfo(m, "abc123", "json")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring("\"name\""))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("formatPluginInfo enriched text", func() {
|
|
var fixedTime time.Time
|
|
|
|
BeforeEach(func() {
|
|
fixedTime = time.Date(2025, 1, 15, 12, 0, 0, 0, time.UTC)
|
|
})
|
|
|
|
It("includes Users, Libraries, Permissions, Created, Updated in text output", func() {
|
|
p := &model.Plugin{
|
|
ID: "myplugin",
|
|
Manifest: `{"name":"My Plugin","version":"1.0.0","author":"me","permissions":{"users":{},"subsonicapi":{}}}`,
|
|
Enabled: true,
|
|
Users: "alice,bob",
|
|
Libraries: "1,2",
|
|
CreatedAt: fixedTime,
|
|
UpdatedAt: fixedTime.Add(time.Hour),
|
|
}
|
|
out, err := formatPluginInfo(p, "text")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring("alice,bob"))
|
|
Expect(out).To(ContainSubstring("1,2"))
|
|
Expect(out).To(ContainSubstring("subsonicapi"))
|
|
Expect(out).To(ContainSubstring("users"))
|
|
Expect(out).To(ContainSubstring("2025-01-15T12:00:00Z"))
|
|
})
|
|
|
|
It("omits Users and Libraries lines when empty", func() {
|
|
p := &model.Plugin{
|
|
ID: "myplugin",
|
|
Manifest: `{"name":"X","version":"1.0.0","author":"me"}`,
|
|
CreatedAt: fixedTime,
|
|
UpdatedAt: fixedTime,
|
|
}
|
|
out, err := formatPluginInfo(p, "text")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).ToNot(ContainSubstring("Users:"))
|
|
Expect(out).ToNot(ContainSubstring("Libraries:"))
|
|
})
|
|
|
|
It("does not alter json output", func() {
|
|
p := &model.Plugin{ID: "x", Manifest: `{}`, Users: "alice"}
|
|
out, err := formatPluginInfo(p, "json")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring(`"x"`))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("formatManifestInfo enriched text", func() {
|
|
It("includes Permissions when declared", func() {
|
|
p := &plugins.Permissions{Http: &plugins.HTTPPermission{}}
|
|
m := &plugins.Manifest{Name: "P", Version: "1.0.0", Author: "a", Permissions: p}
|
|
out, err := formatManifestInfo(m, "abc123", "text")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring("http"))
|
|
Expect(out).To(ContainSubstring("Permissions:"))
|
|
})
|
|
|
|
It("omits Permissions line when no permissions declared", func() {
|
|
m := &plugins.Manifest{Name: "P", Version: "1.0.0", Author: "a"}
|
|
out, err := formatManifestInfo(m, "abc123", "text")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).ToNot(ContainSubstring("Permissions:"))
|
|
})
|
|
|
|
It("does not alter json output", func() {
|
|
p := &plugins.Permissions{Http: &plugins.HTTPPermission{}}
|
|
m := &plugins.Manifest{Name: "P", Version: "1.0.0", Author: "a", Permissions: p}
|
|
out, err := formatManifestInfo(m, "abc123", "json")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(out).To(ContainSubstring(`"name"`))
|
|
})
|
|
})
|
|
|
|
var _ = Describe("rescanPlugins", func() {
|
|
It("calls RescanPlugins on the manager", func() {
|
|
mgr := &tests.MockPluginManager{}
|
|
err := rescanPlugins(context.Background(), mgr)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(mgr.RescanPluginsCalls).To(Equal(1))
|
|
})
|
|
})
|