Files
navidrome-meilisearch/plugins/host_http_permissions_test.go
Dongho Kim c251f174ed
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
update
2025-12-08 16:16:23 +01:00

188 lines
6.8 KiB
Go

package plugins
import (
"github.com/navidrome/navidrome/plugins/schema"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("HTTP Permissions", func() {
Describe("parseHTTPPermissions", func() {
It("should parse valid HTTP permissions", func() {
permData := &schema.PluginManifestPermissionsHttp{
Reason: "Need to fetch album artwork",
AllowLocalNetwork: false,
AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{
"https://api.example.com/*": {
schema.PluginManifestPermissionsHttpAllowedUrlsValueElemGET,
schema.PluginManifestPermissionsHttpAllowedUrlsValueElemPOST,
},
"https://cdn.example.com/*": {
schema.PluginManifestPermissionsHttpAllowedUrlsValueElemGET,
},
},
}
perms, err := parseHTTPPermissions(permData)
Expect(err).To(BeNil())
Expect(perms).ToNot(BeNil())
Expect(perms.AllowLocalNetwork).To(BeFalse())
Expect(perms.AllowedUrls).To(HaveLen(2))
Expect(perms.AllowedUrls["https://api.example.com/*"]).To(Equal([]string{"GET", "POST"}))
Expect(perms.AllowedUrls["https://cdn.example.com/*"]).To(Equal([]string{"GET"}))
})
It("should fail if allowedUrls is empty", func() {
permData := &schema.PluginManifestPermissionsHttp{
Reason: "Need to fetch album artwork",
AllowLocalNetwork: false,
AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{},
}
_, err := parseHTTPPermissions(permData)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("allowedUrls must contain at least one URL pattern"))
})
It("should handle method enum types correctly", func() {
permData := &schema.PluginManifestPermissionsHttp{
Reason: "Need to fetch album artwork",
AllowLocalNetwork: false,
AllowedUrls: map[string][]schema.PluginManifestPermissionsHttpAllowedUrlsValueElem{
"https://api.example.com/*": {
schema.PluginManifestPermissionsHttpAllowedUrlsValueElemWildcard, // "*"
},
},
}
perms, err := parseHTTPPermissions(permData)
Expect(err).To(BeNil())
Expect(perms.AllowedUrls["https://api.example.com/*"]).To(Equal([]string{"*"}))
})
})
Describe("IsRequestAllowed", func() {
var perms *httpPermissions
Context("HTTP method-specific validation", func() {
BeforeEach(func() {
perms = &httpPermissions{
networkPermissionsBase: &networkPermissionsBase{
Reason: "Test permissions",
AllowLocalNetwork: false,
},
AllowedUrls: map[string][]string{
"https://api.example.com": {"GET", "POST"},
"https://upload.example.com": {"PUT", "PATCH"},
"https://admin.example.com": {"DELETE"},
"https://webhook.example.com": {"*"},
},
matcher: newURLMatcher(),
}
})
DescribeTable("method-specific access control",
func(url, method string, shouldSucceed bool) {
err := perms.IsRequestAllowed(url, method)
if shouldSucceed {
Expect(err).ToNot(HaveOccurred())
} else {
Expect(err).To(HaveOccurred())
}
},
// Allowed methods
Entry("GET to api", "https://api.example.com", "GET", true),
Entry("POST to api", "https://api.example.com", "POST", true),
Entry("PUT to upload", "https://upload.example.com", "PUT", true),
Entry("PATCH to upload", "https://upload.example.com", "PATCH", true),
Entry("DELETE to admin", "https://admin.example.com", "DELETE", true),
Entry("any method to webhook", "https://webhook.example.com", "OPTIONS", true),
Entry("any method to webhook", "https://webhook.example.com", "HEAD", true),
// Disallowed methods
Entry("DELETE to api", "https://api.example.com", "DELETE", false),
Entry("GET to upload", "https://upload.example.com", "GET", false),
Entry("POST to admin", "https://admin.example.com", "POST", false),
)
})
Context("case insensitive method handling", func() {
BeforeEach(func() {
perms = &httpPermissions{
networkPermissionsBase: &networkPermissionsBase{
Reason: "Test permissions",
AllowLocalNetwork: false,
},
AllowedUrls: map[string][]string{
"https://api.example.com": {"GET", "POST"}, // Both uppercase for consistency
},
matcher: newURLMatcher(),
}
})
DescribeTable("case insensitive method matching",
func(method string, shouldSucceed bool) {
err := perms.IsRequestAllowed("https://api.example.com", method)
if shouldSucceed {
Expect(err).ToNot(HaveOccurred())
} else {
Expect(err).To(HaveOccurred())
}
},
Entry("uppercase GET", "GET", true),
Entry("lowercase get", "get", true),
Entry("mixed case Get", "Get", true),
Entry("uppercase POST", "POST", true),
Entry("lowercase post", "post", true),
Entry("mixed case Post", "Post", true),
Entry("disallowed method", "DELETE", false),
)
})
Context("with complex URL patterns and HTTP methods", func() {
BeforeEach(func() {
perms = &httpPermissions{
networkPermissionsBase: &networkPermissionsBase{
Reason: "Test permissions",
AllowLocalNetwork: false,
},
AllowedUrls: map[string][]string{
"https://api.example.com/v1/*": {"GET"},
"https://api.example.com/v1/users": {"POST", "PUT"},
"https://*.example.com/public/*": {"GET", "HEAD"},
"https://admin.*.example.com": {"*"},
},
matcher: newURLMatcher(),
}
})
DescribeTable("complex pattern and method combinations",
func(url, method string, shouldSucceed bool) {
err := perms.IsRequestAllowed(url, method)
if shouldSucceed {
Expect(err).ToNot(HaveOccurred())
} else {
Expect(err).To(HaveOccurred())
}
},
// Path wildcards with specific methods
Entry("GET to v1 path", "https://api.example.com/v1/posts", "GET", true),
Entry("POST to v1 path", "https://api.example.com/v1/posts", "POST", false),
Entry("POST to specific users endpoint", "https://api.example.com/v1/users", "POST", true),
Entry("PUT to specific users endpoint", "https://api.example.com/v1/users", "PUT", true),
Entry("DELETE to specific users endpoint", "https://api.example.com/v1/users", "DELETE", false),
// Subdomain wildcards with specific methods
Entry("GET to public path on subdomain", "https://cdn.example.com/public/assets", "GET", true),
Entry("HEAD to public path on subdomain", "https://static.example.com/public/files", "HEAD", true),
Entry("POST to public path on subdomain", "https://api.example.com/public/upload", "POST", false),
// Admin subdomain with all methods
Entry("GET to admin subdomain", "https://admin.prod.example.com", "GET", true),
Entry("POST to admin subdomain", "https://admin.staging.example.com", "POST", true),
Entry("DELETE to admin subdomain", "https://admin.dev.example.com", "DELETE", true),
)
})
})
})