mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-15 05:47:13 +02:00
* fix(plugins): align the Python HTTP example with the repo's host-call pattern
Bind http_send with raw memory offsets like nowplaying-py does, drop
guards for fields the host always sends, and document how plugins
without a PDK call host services and which built-in HTTP APIs are
disabled.
* docs(plugins): document the private-address rules for HTTP requiredHosts
Explain in the README and manifest schema that named hosts can't reach
private addresses while IP/CIDR entries and a bare "*" can.
* docs(plugins): document the private-address rules for requiredHosts
Explain in the README and manifest schema that named hosts can't reach
private addresses while IP/CIDR entries and a bare "*" can, for both
HTTP and WebSocket. Inline the single-use HTTP isHostAllowed wrapper.
* feat(plugins): derive Default for Rust host service structs
The ndpgen client.rs template now adds Default to the derive list of host
service structs, as the capability and shared types templates already do.
Plugin authors can now set only the fields they need, for example
HTTPRequest { method, url, ..Default::default() }. The webhook-rs and
discord-rich-presence-rs examples use this form now. The golden files and
the generated nd-pdk-host crate are updated to match.
* feat(plugins): deprecate pdk.NewHTTPRequest in the Go PDK
Navidrome no longer enables extism's http_request host function, so a
request built with pdk.NewHTTPRequest always fails. ndpgen now reads a small
deprecation table and writes a Deprecated: paragraph for the listed extism
functions, in both the WASM wrapper and the native stub. Linters and IDEs
now point plugin authors to host.HTTPSend. The PDK example tests used to
teach NewHTTPRequest. They now use host.HTTPSend and host.HTTPMock.
* docs(plugins): correct requiredHosts rules for websocket and private addresses
Two statements in the plugin docs did not match the code.
The WebSocket section claimed requiredHosts behaves like HTTP. It does not:
host_httpclient.go only consults the allowlist when the list is non-empty and
otherwise falls back to allowing public addresses, while host_websocket.go
always calls isHostInAllowlist, so an absent list blocks every connection.
The HTTP section claimed a named host can never reach a private address.
checkPrivateDial scans the whole requiredHosts list, so a named host does
reach a private address when the same list also holds a covering IP or CIDR.
Reworded both, plus the matching requiredHosts descriptions in
manifest-schema.json, and regenerated manifest_gen.go.
314 lines
7.7 KiB
Go
314 lines
7.7 KiB
Go
// Example test demonstrating how to use the PDK mock for unit testing.
|
|
// This file is only compiled for non-WASM builds.
|
|
//
|
|
//go:build !wasip1
|
|
|
|
package pdk_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/navidrome/navidrome/plugins/pdk/go/host"
|
|
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
|
|
"github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
// ExamplePlugin demonstrates a simple plugin that uses PDK functions.
|
|
type ExamplePlugin struct{}
|
|
|
|
// ProcessMessage reads input, logs it, and outputs a response.
|
|
func (p *ExamplePlugin) ProcessMessage() error {
|
|
// Get configuration
|
|
prefix, ok := pdk.GetConfig("message_prefix")
|
|
if !ok {
|
|
prefix = "Hello"
|
|
}
|
|
|
|
// Read input
|
|
message := pdk.InputString()
|
|
|
|
// Log the message
|
|
pdk.Log(pdk.LogInfo, "Processing: "+message)
|
|
|
|
// Output the response
|
|
pdk.OutputString(prefix + ", " + message + "!")
|
|
|
|
return nil
|
|
}
|
|
|
|
func TestExamplePlugin_ProcessMessage(t *testing.T) {
|
|
// Reset mock state before the test
|
|
pdk.ResetMock()
|
|
|
|
// Set up expectations
|
|
pdk.PDKMock.On("GetConfig", "message_prefix").Return("Hi", true)
|
|
pdk.PDKMock.On("InputString").Return("World")
|
|
pdk.PDKMock.On("Log", pdk.LogInfo, "Processing: World").Return()
|
|
pdk.PDKMock.On("OutputString", "Hi, World!").Return()
|
|
|
|
// Call the plugin function
|
|
plugin := &ExamplePlugin{}
|
|
err := plugin.ProcessMessage()
|
|
|
|
// Verify no error
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify all expected calls were made
|
|
pdk.PDKMock.AssertExpectations(t)
|
|
}
|
|
|
|
func TestExamplePlugin_ProcessMessage_DefaultPrefix(t *testing.T) {
|
|
// Reset mock state before the test
|
|
pdk.ResetMock()
|
|
|
|
// Set up expectations - config key not found
|
|
pdk.PDKMock.On("GetConfig", "message_prefix").Return("", false)
|
|
pdk.PDKMock.On("InputString").Return("Test")
|
|
pdk.PDKMock.On("Log", pdk.LogInfo, "Processing: Test").Return()
|
|
pdk.PDKMock.On("OutputString", "Hello, Test!").Return()
|
|
|
|
// Call the plugin function
|
|
plugin := &ExamplePlugin{}
|
|
err := plugin.ProcessMessage()
|
|
|
|
// Verify no error
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify all expected calls were made
|
|
pdk.PDKMock.AssertExpectations(t)
|
|
}
|
|
|
|
// Example of testing JSON input/output
|
|
type Request struct {
|
|
Name string `json:"name"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
type Response struct {
|
|
Message string `json:"message"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
func ProcessJSONRequest() error {
|
|
var req Request
|
|
if err := pdk.InputJSON(&req); err != nil {
|
|
pdk.SetError(err)
|
|
return err
|
|
}
|
|
|
|
resp := Response{
|
|
Message: "Hello, " + req.Name,
|
|
Total: req.Count * 2,
|
|
}
|
|
|
|
return pdk.OutputJSON(resp)
|
|
}
|
|
|
|
func TestProcessJSONRequest(t *testing.T) {
|
|
pdk.ResetMock()
|
|
|
|
// Mock InputJSON to populate the request struct
|
|
pdk.PDKMock.On("InputJSON", mock.AnythingOfType("*pdk_test.Request")).
|
|
Return(nil).
|
|
Run(func(args mock.Arguments) {
|
|
req := args.Get(0).(*Request)
|
|
req.Name = "Alice"
|
|
req.Count = 5
|
|
})
|
|
|
|
// Expect OutputJSON with the correct response
|
|
pdk.PDKMock.On("OutputJSON", Response{
|
|
Message: "Hello, Alice",
|
|
Total: 10,
|
|
}).Return(nil)
|
|
|
|
// Call the function
|
|
err := ProcessJSONRequest()
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
pdk.PDKMock.AssertExpectations(t)
|
|
}
|
|
|
|
// =============================================================================
|
|
// HTTP requests go through the host HTTP service (host.HTTPSend), not
|
|
// pdk.NewHTTPRequest, which Navidrome does not enable.
|
|
// =============================================================================
|
|
|
|
// FetchData demonstrates a plugin function that makes an HTTP request.
|
|
func FetchData(url string) ([]byte, error) {
|
|
resp, err := host.HTTPSend(host.HTTPRequest{
|
|
Method: "GET",
|
|
URL: url,
|
|
Headers: map[string]string{
|
|
"Accept": "application/json",
|
|
"User-Agent": "MyPlugin/1.0",
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, nil
|
|
}
|
|
return resp.Body, nil
|
|
}
|
|
|
|
func TestFetchData(t *testing.T) {
|
|
host.HTTPMock.ExpectedCalls = nil
|
|
|
|
expectedBody := []byte(`{"result": "success"}`)
|
|
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
|
|
return req.Method == "GET" && req.URL == "https://api.example.com/data" &&
|
|
req.Headers["Accept"] == "application/json"
|
|
})).Return(&host.HTTPResponse{StatusCode: 200, Body: expectedBody}, nil)
|
|
|
|
body, err := FetchData("https://api.example.com/data")
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if string(body) != string(expectedBody) {
|
|
t.Errorf("expected body %q, got %q", expectedBody, body)
|
|
}
|
|
|
|
host.HTTPMock.AssertExpectations(t)
|
|
}
|
|
|
|
func TestFetchData_NonOKStatus(t *testing.T) {
|
|
host.HTTPMock.ExpectedCalls = nil
|
|
|
|
host.HTTPMock.On("Send", mock.Anything).
|
|
Return(&host.HTTPResponse{StatusCode: 404, Body: []byte("Not Found")}, nil)
|
|
|
|
body, err := FetchData("https://api.example.com/missing")
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Should return nil for non-200 status
|
|
if body != nil {
|
|
t.Errorf("expected nil body for 404, got %q", body)
|
|
}
|
|
|
|
host.HTTPMock.AssertExpectations(t)
|
|
}
|
|
|
|
// ProcessMemoryData demonstrates working with Memory type.
|
|
func ProcessMemoryData(mem pdk.Memory) string {
|
|
// Memory methods work directly on the stub - no mocking needed!
|
|
data := mem.ReadBytes()
|
|
return "Processed " + string(data) + " (length: " + formatUint64(mem.Length()) + ")"
|
|
}
|
|
|
|
func formatUint64(n uint64) string {
|
|
return string(rune('0' + n%10)) // Simplified for demo
|
|
}
|
|
|
|
func TestProcessMemoryData(t *testing.T) {
|
|
// Create stub memory with test data - no mocking needed!
|
|
mem := pdk.NewStubMemory(0, 5, []byte("hello"))
|
|
|
|
result := ProcessMemoryData(mem)
|
|
|
|
expected := "Processed hello (length: 5)"
|
|
if result != expected {
|
|
t.Errorf("expected %q, got %q", expected, result)
|
|
}
|
|
}
|
|
|
|
// StoreAndRetrieve demonstrates Memory Store/Load methods.
|
|
func TestMemoryStoreAndLoad(t *testing.T) {
|
|
// Create empty memory
|
|
mem := pdk.NewStubMemory(100, 0, nil)
|
|
|
|
// Store data - works directly, no mock needed
|
|
mem.Store([]byte("test data"))
|
|
|
|
// Verify the data was stored
|
|
if mem.Length() != 9 {
|
|
t.Errorf("expected length 9, got %d", mem.Length())
|
|
}
|
|
|
|
// Load into buffer
|
|
buffer := make([]byte, 9)
|
|
mem.Load(buffer)
|
|
|
|
if string(buffer) != "test data" {
|
|
t.Errorf("expected 'test data', got %q", buffer)
|
|
}
|
|
|
|
// Free the memory
|
|
mem.Free()
|
|
|
|
if mem.Length() != 0 {
|
|
t.Errorf("expected length 0 after free, got %d", mem.Length())
|
|
}
|
|
}
|
|
|
|
// HTTPMethodString demonstrates that HTTPMethod.String() works without mocking.
|
|
func TestHTTPMethodString(t *testing.T) {
|
|
// These work directly - no mocking needed!
|
|
tests := []struct {
|
|
method pdk.HTTPMethod
|
|
expected string
|
|
}{
|
|
{pdk.MethodGet, "GET"},
|
|
{pdk.MethodPost, "POST"},
|
|
{pdk.MethodPut, "PUT"},
|
|
{pdk.MethodDelete, "DELETE"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
result := tc.method.String()
|
|
if result != tc.expected {
|
|
t.Errorf("expected %q for method %d, got %q", tc.expected, tc.method, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
// PostJSON demonstrates a more complex HTTP request with body.
|
|
func PostJSON(url string, data []byte) (int, error) {
|
|
resp, err := host.HTTPSend(host.HTTPRequest{
|
|
Method: "POST",
|
|
URL: url,
|
|
Headers: map[string]string{"Content-Type": "application/json"},
|
|
Body: data,
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return int(resp.StatusCode), nil
|
|
}
|
|
|
|
func TestPostJSON(t *testing.T) {
|
|
host.HTTPMock.ExpectedCalls = nil
|
|
|
|
host.HTTPMock.On("Send", host.HTTPRequest{
|
|
Method: "POST",
|
|
URL: "https://api.example.com/items",
|
|
Headers: map[string]string{"Content-Type": "application/json"},
|
|
Body: []byte(`{"name":"test"}`),
|
|
}).Return(&host.HTTPResponse{StatusCode: 201}, nil)
|
|
|
|
status, err := PostJSON("https://api.example.com/items", []byte(`{"name":"test"}`))
|
|
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if status != 201 {
|
|
t.Errorf("expected status 201, got %d", status)
|
|
}
|
|
|
|
host.HTTPMock.AssertExpectations(t)
|
|
}
|