mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-31 07:30:32 +00:00
* refactor(artwork): move artworkItemName into core/artwork as ItemName * feat(external): add RefreshInfo to force an external info refresh RefreshInfo re-fetches and re-saves external info for one artist or album, bypassing the TTL check that UpdateArtistInfo/UpdateAlbumInfo use. It is synchronous; callers that must not block detach it themselves. Also makes MockArtistRepo/MockAlbumRepo.UpdateExternalInfo persist to Data (previously a no-op) and adds the new method to the e2e noopProvider, both required so the interface addition compiles and is observable in tests. * feat(external): broadcast RefreshResource after external info is saved populateArtistInfo and populateAlbumInfo now emit the same RefreshResource event the artwork worker uses, so the UI learns about both foreground and background metadata refreshes. * feat(nativeapi): replace artwork refresh endpoint with metadata refresh * feat(ui): add refreshMetadata to the data provider * feat(ui): add a Refresh Metadata item to the album and artist context menus * fix(ui): re-fetch artist info when the record is refreshed * test: fix mislabeled spec, add kind-gate negative case, guard nil mock maps - Rename the RefreshInfo spec that claimed to cover the save-failure/broadcast path: SetError(true) fails Get too, so it only proves RefreshInfo bails out early at getArtist. - Add a spec proving playlist refreshes skip the external-info step, since that asymmetry (al/ar only) was documented but unasserted. - Add lazy nil-map init to MockAlbumRepo/MockArtistRepo.UpdateExternalInfo so a composite-literal-constructed mock doesn't panic on first save. * test: relocate discArtworkName specs from cmd to core/artwork artworkItemName moved into core/artwork as ItemName in an earlier commit, but its disc-name specs stayed behind in cmd/artwork_test.go, reaching across packages. Move them to core/artwork/item_name_test.go where the code now lives. * fix(ui): shape refreshMetadata like a react-admin response react-admin validates custom dataProvider methods and rejects any response without a `data` key, so the raw httpClient promise made every click surface an error toast instead of the success message. The unit test mocked useDataProvider, which skips that validation. Also folds "which kinds have external info" into external.HasInfo so the handler stops restating it, drops the nil-broker guard that only existed for tests, and delegates the mocks' UpdateExternalInfo to Put. * refactor(external): unexport infoKinds Only HasInfo is used outside the package, so the slice itself does not need to be exported. * refactor(artwork): fold ItemName into housekeeping.go next to Refresh ItemName exists to guard Refresh from ids that would orphan a queue row, and both callers invoke them back to back. A separate file hid that pairing; it was only split out to keep the move out of cmd/ legible in review. * fix(nativeapi): return 500 when the refresh lookup fails for a non-ErrNotFound reason A transient repository error told the admin the id did not exist, and the error was dropped without a log line, so nothing pointed at the real cause. Also drops the inherited claim that clearing artwork state shows a placeholder. Reads fall back to local resolution, so that only holds when there is no local art. * fix(ui): move Refresh Metadata above Get Info in the context menu Menu order follows key insertion order in the options object, so the new spec pins the position rather than leaving it to be shuffled by the next addition.
489 lines
16 KiB
Go
489 lines
16 KiB
Go
package nativeapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
|
|
"github.com/navidrome/navidrome/conf"
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
|
"github.com/navidrome/navidrome/consts"
|
|
"github.com/navidrome/navidrome/core/auth"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
"github.com/navidrome/navidrome/server"
|
|
"github.com/navidrome/navidrome/tests"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Plugin API", func() {
|
|
var ds *tests.MockDataStore
|
|
var mockManager *tests.MockPluginManager
|
|
var router http.Handler
|
|
var adminUser, regularUser model.User
|
|
var testPlugin1, testPlugin2 model.Plugin
|
|
|
|
BeforeEach(func() {
|
|
DeferCleanup(configtest.SetupConfig())
|
|
conf.Server.EnableSharing = false
|
|
conf.Server.Plugins.Enabled = true
|
|
ds = &tests.MockDataStore{}
|
|
mockManager = &tests.MockPluginManager{}
|
|
auth.Init(ds)
|
|
nativeRouter := New(ds, nil, nil, nil, tests.NewMockLibraryService(), tests.NewMockUserService(), nil, mockManager, nil, nil)
|
|
router = server.JWTVerifier(nativeRouter)
|
|
|
|
// Create test users
|
|
adminUser = model.User{
|
|
ID: "admin-1",
|
|
UserName: "admin",
|
|
Name: "Admin User",
|
|
IsAdmin: true,
|
|
NewPassword: "adminpass",
|
|
}
|
|
regularUser = model.User{
|
|
ID: "user-1",
|
|
UserName: "regular",
|
|
Name: "Regular User",
|
|
IsAdmin: false,
|
|
NewPassword: "userpass",
|
|
}
|
|
|
|
// Create test plugins
|
|
testPlugin1 = model.Plugin{
|
|
ID: "test-plugin-1",
|
|
Path: "/plugins/test1.wasm",
|
|
Manifest: `{"name":"Test Plugin 1","version":"1.0.0"}`,
|
|
SHA256: "abc123",
|
|
Enabled: false,
|
|
}
|
|
testPlugin2 = model.Plugin{
|
|
ID: "test-plugin-2",
|
|
Path: "/plugins/test2.wasm",
|
|
Manifest: `{"name":"Test Plugin 2","version":"2.0.0"}`,
|
|
Config: `{"setting":"value"}`,
|
|
SHA256: "def456",
|
|
Enabled: true,
|
|
}
|
|
|
|
// Store users in mock datastore
|
|
Expect(ds.User(GinkgoT().Context()).Put(&adminUser)).To(Succeed())
|
|
Expect(ds.User(GinkgoT().Context()).Put(®ularUser)).To(Succeed())
|
|
})
|
|
|
|
Context("when plugins are disabled", func() {
|
|
BeforeEach(func() {
|
|
conf.Server.Plugins.Enabled = false
|
|
})
|
|
|
|
It("returns 404 for all plugin endpoints", func() {
|
|
adminToken, err := auth.CreateToken(&adminUser)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
req := httptest.NewRequest("GET", "/plugin", nil)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusNotFound))
|
|
})
|
|
})
|
|
|
|
Context("when plugins are enabled", func() {
|
|
Describe("as admin user", func() {
|
|
var adminToken string
|
|
|
|
BeforeEach(func() {
|
|
var err error
|
|
adminToken, err = auth.CreateToken(&adminUser)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Store test plugins as admin
|
|
ctx := GinkgoT().Context()
|
|
adminCtx := request.WithUser(ctx, adminUser)
|
|
Expect(ds.Plugin(adminCtx).Put(&testPlugin1)).To(Succeed())
|
|
Expect(ds.Plugin(adminCtx).Put(&testPlugin2)).To(Succeed())
|
|
})
|
|
|
|
Describe("GET /api/plugin", func() {
|
|
It("returns all plugins", func() {
|
|
req := httptest.NewRequest("GET", "/plugin", nil)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
var plugins []model.Plugin
|
|
err := json.Unmarshal(w.Body.Bytes(), &plugins)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(plugins).To(HaveLen(2))
|
|
})
|
|
})
|
|
|
|
Describe("GET /api/plugin/{id}", func() {
|
|
It("returns a specific plugin", func() {
|
|
req := httptest.NewRequest("GET", "/plugin/test-plugin-1", nil)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
var plugin model.Plugin
|
|
err := json.Unmarshal(w.Body.Bytes(), &plugin)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(plugin.ID).To(Equal("test-plugin-1"))
|
|
Expect(plugin.Path).To(Equal("/plugins/test1.wasm"))
|
|
})
|
|
|
|
It("returns 404 for non-existent plugin", func() {
|
|
req := httptest.NewRequest("GET", "/plugin/non-existent", nil)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusNotFound))
|
|
})
|
|
})
|
|
|
|
Describe("PUT /api/plugin/{id}", func() {
|
|
It("updates plugin enabled state", func() {
|
|
// Configure mock to update the repo when EnablePlugin is called
|
|
mockManager.EnablePluginFn = func(ctx context.Context, id string) error {
|
|
adminCtx := request.WithUser(ctx, adminUser)
|
|
p, _ := ds.Plugin(adminCtx).Get(id)
|
|
p.Enabled = true
|
|
return ds.Plugin(adminCtx).Put(p)
|
|
}
|
|
|
|
body := bytes.NewBufferString(`{"enabled":true}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
var plugin model.Plugin
|
|
err := json.Unmarshal(w.Body.Bytes(), &plugin)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(plugin.Enabled).To(BeTrue())
|
|
Expect(mockManager.EnablePluginCalls).To(ContainElement("test-plugin-1"))
|
|
})
|
|
|
|
It("updates plugin config with valid JSON", func() {
|
|
// Configure mock to update the repo when UpdatePluginConfig is called
|
|
mockManager.UpdatePluginConfigFn = func(ctx context.Context, id, configJSON string) error {
|
|
adminCtx := request.WithUser(ctx, adminUser)
|
|
p, _ := ds.Plugin(adminCtx).Get(id)
|
|
p.Config = configJSON
|
|
return ds.Plugin(adminCtx).Put(p)
|
|
}
|
|
|
|
body := bytes.NewBufferString(`{"config":"{\"key\":\"value\"}"}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
var plugin model.Plugin
|
|
err := json.Unmarshal(w.Body.Bytes(), &plugin)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(plugin.Config).To(Equal(`{"key":"value"}`))
|
|
Expect(mockManager.UpdatePluginConfigCalls).To(HaveLen(1))
|
|
Expect(mockManager.UpdatePluginConfigCalls[0].ConfigJSON).To(Equal(`{"key":"value"}`))
|
|
})
|
|
|
|
It("rejects invalid JSON in config field", func() {
|
|
body := bytes.NewBufferString(`{"config":"not valid json"}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
|
Expect(w.Body.String()).To(ContainSubstring("Invalid JSON"))
|
|
})
|
|
|
|
It("allows empty config", func() {
|
|
// Configure mock to update the repo when UpdatePluginConfig is called
|
|
mockManager.UpdatePluginConfigFn = func(ctx context.Context, id, configJSON string) error {
|
|
adminCtx := request.WithUser(ctx, adminUser)
|
|
p, _ := ds.Plugin(adminCtx).Get(id)
|
|
p.Config = configJSON
|
|
return ds.Plugin(adminCtx).Put(p)
|
|
}
|
|
|
|
body := bytes.NewBufferString(`{"config":""}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
var plugin model.Plugin
|
|
err := json.Unmarshal(w.Body.Bytes(), &plugin)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(plugin.Config).To(Equal(""))
|
|
})
|
|
|
|
It("updates users field", func() {
|
|
// Configure mock to update the repo when UpdatePluginUsers is called
|
|
mockManager.UpdatePluginUsersFn = func(ctx context.Context, id, usersJSON string, allUsers bool) error {
|
|
adminCtx := request.WithUser(ctx, adminUser)
|
|
p, _ := ds.Plugin(adminCtx).Get(id)
|
|
p.Users = usersJSON
|
|
p.AllUsers = allUsers
|
|
return ds.Plugin(adminCtx).Put(p)
|
|
}
|
|
|
|
body := bytes.NewBufferString(`{"users":"[\"user1\",\"user2\"]"}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
var plugin model.Plugin
|
|
err := json.Unmarshal(w.Body.Bytes(), &plugin)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(plugin.Users).To(Equal(`["user1","user2"]`))
|
|
Expect(mockManager.UpdatePluginUsersCalls).To(HaveLen(1))
|
|
Expect(mockManager.UpdatePluginUsersCalls[0].UsersJSON).To(Equal(`["user1","user2"]`))
|
|
})
|
|
|
|
It("updates allUsers field", func() {
|
|
// Configure mock to update the repo when UpdatePluginUsers is called
|
|
mockManager.UpdatePluginUsersFn = func(ctx context.Context, id, usersJSON string, allUsers bool) error {
|
|
adminCtx := request.WithUser(ctx, adminUser)
|
|
p, _ := ds.Plugin(adminCtx).Get(id)
|
|
p.Users = usersJSON
|
|
p.AllUsers = allUsers
|
|
return ds.Plugin(adminCtx).Put(p)
|
|
}
|
|
|
|
body := bytes.NewBufferString(`{"allUsers":true}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
var plugin model.Plugin
|
|
err := json.Unmarshal(w.Body.Bytes(), &plugin)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(plugin.AllUsers).To(BeTrue())
|
|
Expect(mockManager.UpdatePluginUsersCalls).To(HaveLen(1))
|
|
Expect(mockManager.UpdatePluginUsersCalls[0].AllUsers).To(BeTrue())
|
|
})
|
|
|
|
It("updates both users and allUsers fields together", func() {
|
|
// Configure mock to update the repo when UpdatePluginUsers is called
|
|
mockManager.UpdatePluginUsersFn = func(ctx context.Context, id, usersJSON string, allUsers bool) error {
|
|
adminCtx := request.WithUser(ctx, adminUser)
|
|
p, _ := ds.Plugin(adminCtx).Get(id)
|
|
p.Users = usersJSON
|
|
p.AllUsers = allUsers
|
|
return ds.Plugin(adminCtx).Put(p)
|
|
}
|
|
|
|
body := bytes.NewBufferString(`{"users":"[\"user1\"]","allUsers":false}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
var plugin model.Plugin
|
|
err := json.Unmarshal(w.Body.Bytes(), &plugin)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(plugin.Users).To(Equal(`["user1"]`))
|
|
Expect(plugin.AllUsers).To(BeFalse())
|
|
Expect(mockManager.UpdatePluginUsersCalls).To(HaveLen(1))
|
|
})
|
|
|
|
It("rejects invalid JSON in users field", func() {
|
|
body := bytes.NewBufferString(`{"users":"not valid json"}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
|
Expect(w.Body.String()).To(ContainSubstring("Invalid JSON"))
|
|
})
|
|
|
|
It("allows empty users", func() {
|
|
// Configure mock to update the repo when UpdatePluginUsers is called
|
|
mockManager.UpdatePluginUsersFn = func(ctx context.Context, id, usersJSON string, allUsers bool) error {
|
|
adminCtx := request.WithUser(ctx, adminUser)
|
|
p, _ := ds.Plugin(adminCtx).Get(id)
|
|
p.Users = usersJSON
|
|
p.AllUsers = allUsers
|
|
return ds.Plugin(adminCtx).Put(p)
|
|
}
|
|
|
|
body := bytes.NewBufferString(`{"users":""}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
|
|
var plugin model.Plugin
|
|
err := json.Unmarshal(w.Body.Bytes(), &plugin)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(plugin.Users).To(Equal(""))
|
|
})
|
|
|
|
It("returns 404 for non-existent plugin", func() {
|
|
body := bytes.NewBufferString(`{"enabled":true}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/non-existent", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusNotFound))
|
|
})
|
|
|
|
It("returns 400 for invalid request body", func() {
|
|
body := bytes.NewBufferString(`not json`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusBadRequest))
|
|
})
|
|
})
|
|
|
|
Describe("POST /api/plugin/rescan", func() {
|
|
It("triggers plugin rescan", func() {
|
|
req := httptest.NewRequest("POST", "/plugin/rescan", nil)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
|
Expect(mockManager.RescanPluginsCalls).To(Equal(1))
|
|
})
|
|
|
|
It("returns error when rescan fails", func() {
|
|
mockManager.RescanError = errors.New("folder not configured")
|
|
|
|
req := httptest.NewRequest("POST", "/plugin/rescan", nil)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+adminToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusInternalServerError))
|
|
Expect(w.Body.String()).To(ContainSubstring("folder not configured"))
|
|
})
|
|
})
|
|
})
|
|
|
|
Describe("as regular user", func() {
|
|
var userToken string
|
|
|
|
BeforeEach(func() {
|
|
var err error
|
|
userToken, err = auth.CreateToken(®ularUser)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
|
|
It("denies access to GET /api/plugin", func() {
|
|
req := httptest.NewRequest("GET", "/plugin", nil)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+userToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusForbidden))
|
|
})
|
|
|
|
It("denies access to GET /api/plugin/{id}", func() {
|
|
req := httptest.NewRequest("GET", "/plugin/test-plugin-1", nil)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+userToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusForbidden))
|
|
})
|
|
|
|
It("denies access to PUT /api/plugin/{id}", func() {
|
|
body := bytes.NewBufferString(`{"enabled":true}`)
|
|
req := httptest.NewRequest("PUT", "/plugin/test-plugin-1", body)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+userToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusForbidden))
|
|
})
|
|
|
|
It("denies access to POST /api/plugin/rescan", func() {
|
|
req := httptest.NewRequest("POST", "/plugin/rescan", nil)
|
|
req.Header.Set(consts.UIAuthorizationHeader, "Bearer "+userToken)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusForbidden))
|
|
})
|
|
})
|
|
|
|
Describe("without authentication", func() {
|
|
It("denies access to plugin endpoints", func() {
|
|
req := httptest.NewRequest("GET", "/plugin", nil)
|
|
w := httptest.NewRecorder()
|
|
|
|
router.ServeHTTP(w, req)
|
|
|
|
Expect(w.Code).To(Equal(http.StatusUnauthorized))
|
|
})
|
|
})
|
|
})
|
|
})
|