mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 07:45:30 +00:00
327 lines
8.7 KiB
Go
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)
|
|
}
|
|
}
|