http_test.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. package agentmcp
  2. import (
  3. "net/http"
  4. "net/http/httptest"
  5. "reflect"
  6. "regexp"
  7. "strings"
  8. "testing"
  9. "go.uber.org/zap"
  10. )
  11. var toolNameRE = regexp.MustCompile(`"name":"([a-z_]+)"`)
  12. // listTools 以 tools/list 请求 path,返回状态码与工具名集合。
  13. func listTools(t *testing.T, h http.Handler, path, token string) (int, map[string]bool) {
  14. t.Helper()
  15. req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{"jsonrpc":"2.0","id":1,"method":"tools/list"}`))
  16. req.Host = "127.0.0.1:8091"
  17. req.Header.Set("Content-Type", "application/json")
  18. req.Header.Set("Accept", "application/json, text/event-stream")
  19. req.Header.Set("Authorization", "Bearer "+token)
  20. rec := httptest.NewRecorder()
  21. h.ServeHTTP(rec, req)
  22. names := map[string]bool{}
  23. for _, m := range toolNameRE.FindAllStringSubmatch(rec.Body.String(), -1) {
  24. names[m[1]] = true
  25. }
  26. return rec.Code, names
  27. }
  28. // 一个端口按路径区分工具集:各路径只暴露自己的工具,令牌各自校验;/mcp 兼容为只读工具集。
  29. func TestHandlerRoutesSetsByPath(t *testing.T) {
  30. log := zap.NewNop()
  31. var endpoints []Endpoint
  32. tokens := map[string]string{SetReadOnly: "tok-ro", SetOps: "tok-ops"}
  33. for _, set := range AllSets {
  34. s, tasks, err := NewServer(set, log, 2)
  35. if err != nil {
  36. t.Fatal(err)
  37. }
  38. endpoints = append(endpoints, Endpoint{Set: set, Server: s, Tasks: tasks, Token: tokens[set]})
  39. }
  40. h := Handler(endpoints, log)
  41. roCount := len(mustTools(t, SetReadOnly))
  42. opsCount := len(mustTools(t, SetOps))
  43. for _, path := range []string{"/mcp", "/mcp/readonly"} {
  44. code, names := listTools(t, h, path, "tok-ro")
  45. if code != http.StatusOK || len(names) != roCount || !names["site_search"] || names["site_info_refresh"] {
  46. t.Errorf("%s: code=%d 工具数=%d,期望 200 与只读工具集 %d 个", path, code, len(names), roCount)
  47. }
  48. }
  49. code, names := listTools(t, h, "/mcp/ops", "tok-ops")
  50. if code != http.StatusOK || len(names) != opsCount || !names["site_info_refresh"] || names["site_search"] {
  51. t.Errorf("/mcp/ops: code=%d 工具数=%d,期望 200 与写工具集 %d 个", code, len(names), opsCount)
  52. }
  53. if code, _ := listTools(t, h, "/mcp/ops", "tok-ro"); code != http.StatusUnauthorized {
  54. t.Errorf("/mcp/ops 用只读令牌应返回 401,实际 %d", code)
  55. }
  56. if code, _ := listTools(t, h, "/mcp/readonly", "tok-ops"); code != http.StatusUnauthorized {
  57. t.Errorf("/mcp/readonly 用写令牌应返回 401,实际 %d", code)
  58. }
  59. if code, _ := listTools(t, h, "/mcp/unknown", "tok-ro"); code != http.StatusNotFound {
  60. t.Errorf("/mcp/unknown 应返回 404,实际 %d", code)
  61. }
  62. }
  63. // 只加载只读工具集时不暴露 /mcp/ops。
  64. func TestHandlerOnlyMountsLoadedSets(t *testing.T) {
  65. log := zap.NewNop()
  66. s, tasks, err := NewServer(SetReadOnly, log, 2)
  67. if err != nil {
  68. t.Fatal(err)
  69. }
  70. h := Handler([]Endpoint{{Set: SetReadOnly, Server: s, Tasks: tasks, Token: "tok"}}, log)
  71. if code, _ := listTools(t, h, "/mcp/ops", "tok"); code != http.StatusNotFound {
  72. t.Errorf("未加载 ops 时 /mcp/ops 应返回 404,实际 %d", code)
  73. }
  74. }
  75. func TestParseSets(t *testing.T) {
  76. cases := []struct {
  77. in string
  78. want []string
  79. wantErr bool
  80. }{
  81. {"", AllSets, false},
  82. {"readonly", []string{SetReadOnly}, false},
  83. {" ops , readonly ,ops", []string{SetOps, SetReadOnly}, false},
  84. {"readonly,foo", nil, true},
  85. {" , ", nil, true},
  86. }
  87. for _, c := range cases {
  88. got, err := ParseSets(c.in)
  89. if (err != nil) != c.wantErr || (!c.wantErr && !reflect.DeepEqual(got, c.want)) {
  90. t.Errorf("ParseSets(%q) = %v, %v;期望 %v, err=%v", c.in, got, err, c.want, c.wantErr)
  91. }
  92. }
  93. }
  94. func TestTokenFor(t *testing.T) {
  95. s := Settings{Token: "base", SetTokens: map[string]string{SetOps: "ops-only"}}
  96. if s.TokenFor(SetReadOnly) != "base" || s.TokenFor(SetOps) != "ops-only" {
  97. t.Errorf("TokenFor 结果不符:readonly=%s ops=%s", s.TokenFor(SetReadOnly), s.TokenFor(SetOps))
  98. }
  99. }
  100. func mustTools(t *testing.T, set string) []string {
  101. t.Helper()
  102. list, err := Tools(set)
  103. if err != nil {
  104. t.Fatal(err)
  105. }
  106. names := make([]string, 0, len(list))
  107. for _, r := range list {
  108. names = append(names, r.Meta().Name)
  109. }
  110. return names
  111. }