Files
gopeed/pkg/rest/invoke_test.go
T

327 lines
8.7 KiB
Go

package rest
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
func TestNativeInvokeWithoutRESTListener(t *testing.T) {
cfg := &model.StartConfig{
NativeMode: true,
Storage: model.StorageMem,
}
port, err := Start(cfg)
if err != nil {
t.Fatal(err)
}
defer Stop()
if port != 0 {
t.Fatalf("native runtime opened REST port %d by default", port)
}
var result model.Result[json.RawMessage]
if err := json.Unmarshal([]byte(Dispatch(http.MethodGet, "/api/v1/info", "", "")), &result); err != nil {
t.Fatal(err)
}
if result.Code != model.CodeOk {
t.Fatalf("invoke failed: %#v", result)
}
}
func TestStartAPIServerIsNotBlockedByPendingDispatch(t *testing.T) {
requestStarted := make(chan struct{})
releaseRequest := make(chan struct{})
var releaseOnce sync.Once
release := func() { releaseOnce.Do(func() { close(releaseRequest) }) }
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
close(requestStarted)
<-releaseRequest
w.Header().Set("Content-Length", "0")
}))
t.Cleanup(func() {
release()
upstream.Close()
Stop()
})
if _, err := Start(&model.StartConfig{NativeMode: true, Storage: model.StorageMem}); err != nil {
t.Fatal(err)
}
requestBody, err := json.Marshal(&model.ResolveTask{Req: &base.Request{URL: upstream.URL}})
if err != nil {
t.Fatal(err)
}
dispatchDone := make(chan struct{})
go func() {
defer close(dispatchDone)
Dispatch(http.MethodPost, "/api/v1/resolve", "", string(requestBody))
}()
select {
case <-requestStarted:
case <-time.After(time.Second):
t.Fatal("resolve request did not start")
}
config, err := Downloader.GetConfig()
if err != nil {
t.Fatal(err)
}
config.API = &base.APIServerConfig{Enable: true, Network: "tcp", Address: "127.0.0.1:0"}
if err := Downloader.PutConfig(config); err != nil {
t.Fatal(err)
}
started := make(chan *model.APIServerState, 1)
startErrors := make(chan error, 1)
go func() {
state, err := StartAPIServer()
started <- state
startErrors <- err
}()
select {
case state := <-started:
if err := <-startErrors; err != nil {
t.Fatalf("start API server failed: %v", err)
}
if !state.Running || state.RunningPort == 0 {
t.Fatalf("API server did not start: %#v", state)
}
case <-time.After(time.Second):
t.Fatal("pending Resolve blocked API server startup")
}
release()
select {
case <-dispatchDone:
case <-time.After(time.Second):
t.Fatal("resolve request did not finish after release")
}
}
func TestAPIServerLifecycleUsesPersistedConfigWithoutRestartingCore(t *testing.T) {
port, err := Start(&model.StartConfig{NativeMode: true, Storage: model.StorageMem})
if err != nil {
t.Fatal(err)
}
defer Stop()
if port != 0 {
t.Fatalf("native runtime opened REST port %d by default", port)
}
putAPIServerConfig(t, &base.APIServerConfig{
Enable: true,
MCPEnable: true,
Network: "tcp",
Address: "127.0.0.1:0",
Token: "secret",
})
state, err := StartAPIServer()
if err != nil {
t.Fatal(err)
}
port = state.RunningPort
if port == 0 {
t.Fatal("enabling the REST API did not open a TCP listener")
}
if !state.MCPEnabled {
t.Fatal("persisted MCP endpoint setting was not applied")
}
request, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/api/v1/info", port), nil)
if err != nil {
t.Fatal(err)
}
request.Header.Set("X-Api-Token", "secret")
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusOK {
t.Fatalf("REST API returned status %d", response.StatusCode)
}
mcpRequest, err := http.NewRequest(
http.MethodPost,
fmt.Sprintf("http://127.0.0.1:%d/mcp", port),
strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"gopeed-test","version":"1.0.0"}}}`),
)
if err != nil {
t.Fatal(err)
}
mcpRequest.Header.Set("Accept", "application/json, text/event-stream")
mcpRequest.Header.Set("Content-Type", "application/json")
mcpRequest.Header.Set("Authorization", "Bearer secret")
mcpResponse, err := http.DefaultClient.Do(mcpRequest)
if err != nil {
t.Fatal(err)
}
mcpResponse.Body.Close()
if mcpResponse.StatusCode != http.StatusOK {
t.Fatalf("MCP endpoint returned status %d", mcpResponse.StatusCode)
}
putAPIServerConfig(t, &base.APIServerConfig{Enable: false})
if state, err := StopAPIServer(); err != nil {
t.Fatal(err)
} else if state.Running || state.MCPEnabled || state.RunningPort != 0 || state.PendingApply {
t.Fatalf("unexpected stopped API server state: %#v", state)
}
var result model.Result[json.RawMessage]
if err := json.Unmarshal([]byte(Dispatch(http.MethodGet, "/api/v1/info", "", "")), &result); err != nil {
t.Fatal(err)
}
if result.Code != model.CodeOk {
t.Fatalf("in-process API stopped with REST listener: %#v", result)
}
if _, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/v1/info", port)); err == nil {
t.Fatal("REST listener still accepted connections after it was disabled")
}
}
func TestRestartAPIServerFailsFastAndLeavesListenerStopped(t *testing.T) {
if _, err := Start(&model.StartConfig{NativeMode: true, Storage: model.StorageMem}); err != nil {
t.Fatal(err)
}
defer Stop()
putAPIServerConfig(t, &base.APIServerConfig{
Enable: true,
Network: "tcp",
Address: "127.0.0.1:0",
Token: "secret",
})
startedState, err := StartAPIServer()
if err != nil {
t.Fatal(err)
}
previousPort := startedState.RunningPort
putAPIServerConfig(t, &base.APIServerConfig{
Enable: true,
Network: "unsupported-network",
Address: "invalid",
})
state, err := RestartAPIServer()
if err == nil {
t.Fatal("invalid listener configuration unexpectedly succeeded")
}
if state.Running || state.RunningPort != 0 || !state.PendingApply || state.LastError == "" {
t.Fatalf("failed restart did not leave REST listener stopped: %#v", state)
}
if _, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/v1/info", previousPort)); err == nil {
t.Fatal("previous REST listener still accepted connections after failed restart")
}
var result model.Result[json.RawMessage]
if err := json.Unmarshal([]byte(Dispatch(http.MethodGet, "/api/v1/info", "", "")), &result); err != nil {
t.Fatal(err)
}
if result.Code != model.CodeOk {
t.Fatalf("in-process API unavailable after failed REST restart: %#v", result)
}
}
func TestNativeRESTSettingPersistsInGoStorage(t *testing.T) {
storageDir := filepath.Join(t.TempDir(), "storage")
start := func() int {
port, err := Start(&model.StartConfig{
NativeMode: true,
Storage: model.StorageBolt,
StorageDir: storageDir,
})
if err != nil {
t.Fatal(err)
}
return port
}
if port := start(); port != 0 {
t.Fatalf("first native start opened REST port %d", port)
}
config, err := Downloader.GetConfig()
if err != nil {
t.Fatal(err)
}
config.API = newAPIServerConfigForTest()
if err := Downloader.PutConfig(config); err != nil {
t.Fatal(err)
}
Stop()
if port := start(); port == 0 {
Stop()
t.Fatal("persisted REST setting did not open a listener")
}
Stop()
}
func TestNativeAPIServerAutoStartFailureKeepsCoreAvailable(t *testing.T) {
storageDir := filepath.Join(t.TempDir(), "storage")
startConfig := &model.StartConfig{
NativeMode: true,
Storage: model.StorageBolt,
StorageDir: storageDir,
}
if _, err := Start(startConfig); err != nil {
t.Fatal(err)
}
putAPIServerConfig(t, &base.APIServerConfig{
Enable: true,
Network: "unsupported-network",
Address: "invalid",
})
Stop()
port, err := Start(startConfig)
if err != nil {
t.Fatalf("optional REST listener failure stopped native core startup: %v", err)
}
defer Stop()
if port != 0 {
t.Fatalf("failed REST listener reported running port %d", port)
}
state, err := GetAPIServerState()
if err != nil {
t.Fatal(err)
}
if !state.Enabled || state.Running || !state.PendingApply || state.LastError == "" {
t.Fatalf("unexpected state after optional REST listener failure: %#v", state)
}
var result model.Result[json.RawMessage]
if err := json.Unmarshal([]byte(Dispatch(http.MethodGet, "/api/v1/info", "", "")), &result); err != nil {
t.Fatal(err)
}
if result.Code != model.CodeOk {
t.Fatalf("in-process API unavailable after REST listener failure: %#v", result)
}
}
func newAPIServerConfigForTest() *base.APIServerConfig {
return &base.APIServerConfig{Enable: true, Network: "tcp", Address: "127.0.0.1:0"}
}
func putAPIServerConfig(t *testing.T, apiConfig *base.APIServerConfig) {
t.Helper()
config, err := Downloader.GetConfig()
if err != nil {
t.Fatal(err)
}
config.API = apiConfig
if err := Downloader.PutConfig(config); err != nil {
t.Fatal(err)
}
}