proxy_test.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. package agentserver
  2. import (
  3. "io"
  4. "net/http"
  5. "net/http/httptest"
  6. "strings"
  7. "sync"
  8. "testing"
  9. "github.com/gofiber/fiber/v2"
  10. )
  11. type lockedSink struct {
  12. mu sync.Mutex
  13. got []Resource
  14. }
  15. func (s *lockedSink) Record(rs []Resource) {
  16. s.mu.Lock()
  17. defer s.mu.Unlock()
  18. s.got = append(s.got, rs...)
  19. }
  20. func (s *lockedSink) ids() []string {
  21. s.mu.Lock()
  22. defer s.mu.Unlock()
  23. var out []string
  24. for _, r := range s.got {
  25. out = append(out, r.Id)
  26. }
  27. return out
  28. }
  29. // 假的 AgentServer:记录收到的鉴权头与请求体
  30. func fakeAgentServer(t *testing.T, seen *http.Header, lastBody *string) *httptest.Server {
  31. return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  32. *seen = r.Header.Clone()
  33. b, _ := io.ReadAll(r.Body)
  34. *lastBody = string(b)
  35. switch {
  36. case r.URL.Path == "/v1/sessions/ses_a1/stream":
  37. w.Header().Set("Content-Type", "text/event-stream")
  38. w.WriteHeader(200)
  39. io.WriteString(w, "retry: 2000\n\nid: 1\nevent: run.started\ndata: "+
  40. `{"seq":1,"type":"run.started","sessionId":"ses_a1","runId":"run_s1","payload":{}}`+"\n\n")
  41. case r.URL.Path == "/v1/sessions/ses_a1/messages":
  42. w.Header().Set("Content-Type", "application/json")
  43. w.WriteHeader(202)
  44. io.WriteString(w, `{"id":"run_m1","sessionId":"ses_a1","status":"queued"}`)
  45. case r.URL.Path == "/v1/sessions/ses_a1":
  46. w.Header().Set("Content-Type", "application/json")
  47. io.WriteString(w, `{"id":"ses_a1","title":"x"}`)
  48. case r.URL.Path == "/v1/files/file_a":
  49. w.Header().Set("Content-Type", "text/plain")
  50. w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''a.txt")
  51. io.WriteString(w, "file-content")
  52. default:
  53. w.WriteHeader(404)
  54. io.WriteString(w, `{"error":{"code":"not_found","message":"x"}}`)
  55. }
  56. }))
  57. }
  58. func newTestApp(t *testing.T, upstreamURL string, sink *lockedSink) (*fiber.App, *[]string) {
  59. var touched []string
  60. guard := &Guard{
  61. Store: &fakeStore{
  62. owners: map[string]string{"ses_a1": "alice", "ses_b1": "bob"},
  63. resources: map[string]string{"file_a": "ses_a1"},
  64. },
  65. Upstream: &fakeUpstream{},
  66. Sink: sink,
  67. }
  68. p := &Proxy{
  69. Client: NewClient(upstreamURL, "ask_test"),
  70. Guard: guard,
  71. Sink: sink,
  72. OnMessage: func(uid, sid string) { touched = append(touched, uid+"@"+sid) },
  73. }
  74. app := fiber.New()
  75. app.All("/agent/v1/*", func(c *fiber.Ctx) error { return p.Handle(c, "alice", c.Params("*")) })
  76. return app, &touched
  77. }
  78. func TestProxy(t *testing.T) {
  79. var seen http.Header
  80. var body string
  81. up := fakeAgentServer(t, &seen, &body)
  82. defer up.Close()
  83. sink := &lockedSink{}
  84. app, touched := newTestApp(t, up.URL, sink)
  85. do := func(method, path, reqBody string) (*http.Response, string) {
  86. req := httptest.NewRequest(method, path, strings.NewReader(reqBody))
  87. req.Header.Set("Authorization", "Bearer toolbox-token")
  88. req.Header.Set("Cookie", "secret=1")
  89. if reqBody != "" {
  90. req.Header.Set("Content-Type", "application/json")
  91. }
  92. resp, err := app.Test(req, 5000)
  93. if err != nil {
  94. t.Fatal(err)
  95. }
  96. b, _ := io.ReadAll(resp.Body)
  97. return resp, string(b)
  98. }
  99. // 白名单外
  100. if resp, _ := do("GET", "/agent/v1/sessions", ""); resp.StatusCode != 403 {
  101. t.Errorf("GET /sessions 应为 403,得到 %d", resp.StatusCode)
  102. }
  103. // 他人会话
  104. if resp, b := do("GET", "/agent/v1/sessions/ses_b1/events", ""); resp.StatusCode != 404 || !strings.Contains(b, "not_found") {
  105. t.Errorf("他人会话应为 404,得到 %d %s", resp.StatusCode, b)
  106. }
  107. // 发送消息:替换鉴权头、不转发 Cookie、记录 runId、刷新活跃时间
  108. resp, b := do("POST", "/agent/v1/sessions/ses_a1/messages", `{"text":"hi"}`)
  109. if resp.StatusCode != 202 || !strings.Contains(b, "run_m1") {
  110. t.Fatalf("发送消息 %d %s", resp.StatusCode, b)
  111. }
  112. if seen.Get("Authorization") != "Bearer ask_test" || seen.Get("Cookie") != "" {
  113. t.Errorf("上游请求头 Authorization=%q Cookie=%q", seen.Get("Authorization"), seen.Get("Cookie"))
  114. }
  115. if len(*touched) != 1 || (*touched)[0] != "alice@ses_a1" {
  116. t.Errorf("OnMessage %v", *touched)
  117. }
  118. // PATCH 只保留 title、model
  119. do("PATCH", "/agent/v1/sessions/ses_a1", `{"title":"t","permissionMode":"auto"}`)
  120. if body != `{"title":"t"}` {
  121. t.Errorf("PATCH 转发的请求体 %s", body)
  122. }
  123. // 文件下载透传
  124. resp, b = do("GET", "/agent/v1/files/file_a?inline=1", "")
  125. if resp.StatusCode != 200 || b != "file-content" || resp.Header.Get("Content-Disposition") == "" {
  126. t.Errorf("文件下载 %d %q %v", resp.StatusCode, b, resp.Header)
  127. }
  128. // SSE 透传并记录 runId
  129. resp, b = do("GET", "/agent/v1/sessions/ses_a1/stream", "")
  130. if resp.StatusCode != 200 || !strings.Contains(b, "run.started") || resp.Header.Get("X-Accel-Buffering") != "no" {
  131. t.Errorf("SSE %d %q", resp.StatusCode, b)
  132. }
  133. ids := strings.Join(sink.ids(), ",")
  134. if !strings.Contains(ids, "run_m1") || !strings.Contains(ids, "run_s1") {
  135. t.Errorf("记录的子资源 %s", ids)
  136. }
  137. }