返回 DeepSeek-Reasonix
launch_token_test.go
根目录 / internal / serve / launch_token_test.go
1 package serve
2
3 import (
4 "encoding/json"
5 "maps"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10
11 "reasonix/internal/config"
12 "reasonix/internal/control"
13 )
14
15 var mutationRoutes = []string{
16 "/submit",
17 "/composer-profile",
18 "/handoff",
19 "/approve",
20 "/plan-decision",
21 "/answer",
22 "/resolve-prompt",
23 "/mcp-interaction",
24 "/extension-form",
25 "/tool-approval-mode",
26 "/auto-approve-tools",
27 "/bypass",
28 "/permission/preset",
29 }
30
31 // operatorHandler stands in for the operator's frontend, which holds the
32 // launch token that mutations require.
33 func operatorHandler(s *Server) http.Handler {
34 h := s.Handler()
35 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
36 r.Header.Set("Authorization", "Bearer "+s.AuthToken())
37 h.ServeHTTP(w, r)
38 })
39 }
40
41 func postJSON(t *testing.T, url, body string, header http.Header) *http.Response {
42 t.Helper()
43 req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(body))
44 if err != nil {
45 t.Fatal(err)
46 }
47 req.Header.Set("Content-Type", "application/json")
48 maps.Copy(req.Header, header)
49 resp, err := http.DefaultClient.Do(req)
50 if err != nil {
51 t.Fatal(err)
52 }
53 return resp
54 }
55
56 func TestAuthDisabledRefusesMutationsWithoutLaunchToken(t *testing.T) {
57 bc := NewBroadcaster()
58 ctrl := control.New(control.Options{Sink: bc})
59 srv := httptest.NewServer(New(ctrl, bc, config.ServeConfig{AuthMode: "none"}).Handler())
60 defer srv.Close()
61
62 for _, route := range mutationRoutes {
63 resp := postJSON(t, srv.URL+route, `{"id":"1","allow":true,"session":true}`, nil)
64 var body struct {
65 Code string `json:"code"`
66 }
67 _ = json.NewDecoder(resp.Body).Decode(&body)
68 resp.Body.Close()
69 if resp.StatusCode != http.StatusForbidden || body.Code != launchTokenRequiredCode {
70 t.Errorf("POST %s without launch token = %d code=%q, want 403 %q", route, resp.StatusCode, body.Code, launchTokenRequiredCode)
71 }
72 }
73 resp, err := http.Get(srv.URL + "/pending-prompts")
74 if err != nil {
75 t.Fatal(err)
76 }
77 resp.Body.Close()
78 if resp.StatusCode != http.StatusOK {
79 t.Fatalf("GET /pending-prompts with auth disabled = %d, want 200: reads stay open", resp.StatusCode)
80 }
81 }
82
83 func TestAuthDisabledHonoursConfiguredToken(t *testing.T) {
84 s := New(control.New(control.Options{Sink: NewBroadcaster()}), nil, config.ServeConfig{AuthMode: "none", Token: "proxy-held"})
85 if s.AuthToken() != "proxy-held" {
86 t.Fatalf("launch token = %q, want the configured token a fronting proxy injects", s.AuthToken())
87 }
88 }
89
90 func TestAuthDisabledAcceptsHumanDecisionWithLaunchToken(t *testing.T) {
91 bc := NewBroadcaster()
92 ctrl := control.New(control.Options{Sink: bc})
93 s := New(ctrl, bc, config.ServeConfig{AuthMode: "none"})
94 srv := httptest.NewServer(s.Handler())
95 defer srv.Close()
96 if s.AuthToken() == "" {
97 t.Fatal("auth-disabled serve has no launch token")
98 }
99
100 resp := postJSON(t, srv.URL+"/approve", `{"allow":true}`, http.Header{"Authorization": {"Bearer " + s.AuthToken()}})
101 resp.Body.Close()
102 if resp.StatusCode != http.StatusBadRequest {
103 t.Fatalf("POST /approve with bearer launch token = %d, want the handler's 400 for a missing id", resp.StatusCode)
104 }
105
106 resp = postJSON(t, srv.URL+"/auth/token", `{"token":"`+s.AuthToken()+`"}`, nil)
107 resp.Body.Close()
108 var cookie *http.Cookie
109 for _, c := range resp.Cookies() {
110 if c.Name == cookieToken {
111 cookie = c
112 }
113 }
114 if resp.StatusCode != http.StatusNoContent || cookie == nil {
115 t.Fatalf("launch token bootstrap = %d cookie=%v, want 204 with %s", resp.StatusCode, cookie, cookieToken)
116 }
117 resp = postJSON(t, srv.URL+"/approve", `{"allow":true}`, http.Header{"Cookie": {cookie.Name + "=" + cookie.Value}})
118 resp.Body.Close()
119 if resp.StatusCode != http.StatusBadRequest {
120 t.Fatalf("POST /approve with launch token cookie = %d, want the handler's 400 for a missing id", resp.StatusCode)
121 }
122
123 resp = postJSON(t, srv.URL+"/approve", `{"allow":true}`, http.Header{"Authorization": {"Bearer wrong"}})
124 resp.Body.Close()
125 if resp.StatusCode != http.StatusForbidden {
126 t.Fatalf("POST /approve with a wrong token = %d, want 403", resp.StatusCode)
127 }
128 }
129
130 func TestTokenModeAcceptsBearerLaunchToken(t *testing.T) {
131 bc := NewBroadcaster()
132 ctrl := control.New(control.Options{Sink: bc})
133 s := New(ctrl, bc, config.ServeConfig{AuthMode: "token"})
134 srv := httptest.NewServer(s.Handler())
135 defer srv.Close()
136
137 resp := postJSON(t, srv.URL+"/approve", `{"id":"1","allow":true}`, nil)
138 resp.Body.Close()
139 if resp.StatusCode != http.StatusUnauthorized {
140 t.Fatalf("unauthenticated POST /approve in token mode = %d, want 401", resp.StatusCode)
141 }
142 resp = postJSON(t, srv.URL+"/approve", `{"allow":true}`, http.Header{"Authorization": {"Bearer " + s.AuthToken()}})
143 resp.Body.Close()
144 if resp.StatusCode != http.StatusBadRequest {
145 t.Fatalf("POST /approve with bearer token = %d, want the handler's 400 for a missing id", resp.StatusCode)
146 }
147 }
148
148 lines GO