返回 DeepSeek-Reasonix
cred_proxy_test.go
根目录 / desktop / cred_proxy_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "net/http"
9 "net/http/httptest"
10 "net/url"
11 "slices"
12 "strings"
13 "sync"
14 "testing"
15
16 "reasonix/internal/config"
17 "reasonix/internal/remote/bootstrap"
18 )
19
20 type failingRequestBody struct{}
21
22 func (failingRequestBody) Read([]byte) (int, error) { return 0, errors.New("read failed") }
23 func (failingRequestBody) Close() error { return nil }
24
25 func mustParseURL(t *testing.T, raw string) *url.URL {
26 t.Helper()
27 u, err := url.Parse(strings.TrimRight(raw, "/") + "/")
28 if err != nil {
29 t.Fatal(err)
30 }
31 return u
32 }
33
34 // TestCredentialProxyAuthSwap covers the desktop key holder over real HTTP:
35 // the registered virtual token forwards to the provider with the real key,
36 // anything else is rejected without reaching the provider.
37 func TestCredentialProxyAuthSwap(t *testing.T) {
38 var gotAuth, gotForwarded string
39 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
40 gotAuth = r.Header.Get("Authorization")
41 for _, h := range []string{"Forwarded", "X-Forwarded-For", "X-Forwarded-Host", "X-Forwarded-Proto", "X-Real-IP", "Via"} {
42 gotForwarded += r.Header.Get(h)
43 }
44 _, _ = w.Write([]byte("model-ok"))
45 }))
46 defer upstream.Close()
47
48 seedBridgeTestHost(t, "box")
49 a := &App{}
50 t.Cleanup(a.closeCredentialProxy)
51 port, err := a.credentialProxyPort()
52 if err != nil {
53 t.Fatal(err)
54 }
55 const token = "virtual-tok"
56 a.credProxy.setRoute(token, "", mustParseURL(t, upstream.URL), "sk-real-key", "", "")
57 proxyURL := fmt.Sprintf("http://127.0.0.1:%d/v1/chat", port)
58
59 do := func(auth string) (int, string) {
60 req, err := http.NewRequest(http.MethodPost, proxyURL, strings.NewReader("{}"))
61 if err != nil {
62 t.Fatal(err)
63 }
64 req.Header.Set("Authorization", auth)
65 req.Header.Set("Connection", "Authorization")
66 req.Header.Set("Forwarded", "for=attacker")
67 req.Header.Set("X-Forwarded-For", "203.0.113.9")
68 resp, err := http.DefaultClient.Do(req)
69 if err != nil {
70 t.Fatal(err)
71 }
72 defer resp.Body.Close()
73 buf := make([]byte, 64)
74 n, _ := resp.Body.Read(buf)
75 return resp.StatusCode, string(buf[:n])
76 }
77
78 if code, body := do("Bearer virtual-tok"); code != 200 || body != "model-ok" {
79 t.Fatalf("valid token: code=%d body=%q", code, body)
80 }
81 if gotAuth != "Bearer sk-real-key" {
82 t.Fatalf("upstream auth = %q, want the real key", gotAuth)
83 }
84 if gotForwarded != "" {
85 t.Fatalf("forwarding identity leaked upstream: %q", gotForwarded)
86 }
87 if code, _ := do("Bearer wrong"); code != 401 {
88 t.Fatalf("wrong token: code=%d, want 401", code)
89 }
90 if gotAuth != "Bearer sk-real-key" {
91 t.Fatalf("rejected request reached the upstream: %q", gotAuth)
92 }
93 if code, _ := do(""); code != 401 {
94 t.Fatalf("missing token: code=%d, want 401", code)
95 }
96 }
97
98 // TestCredentialProxyRewritesRequestModel: desktop owns the current model, so
99 // the proxy replaces the serve's request-body model with the desktop selection
100 // before the real provider sees it. The provider must also see its OWN host
101 // in the Host header — the inbound loopback host must not leak through
102 // (CloudFront-fronted APIs 403 a foreign Host).
103 func TestCredentialProxyRewritesRequestModel(t *testing.T) {
104 var gotBody, gotHost string
105 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
106 buf, _ := io.ReadAll(r.Body)
107 gotBody = string(buf)
108 gotHost = r.Host
109 _, _ = w.Write([]byte("model-ok"))
110 }))
111 defer upstream.Close()
112
113 seedBridgeTestHost(t, "box")
114 a := &App{}
115 t.Cleanup(a.closeCredentialProxy)
116 port, err := a.credentialProxyPort()
117 if err != nil {
118 t.Fatal(err)
119 }
120 const token = "virtual-tok"
121 a.credProxy.setRoute(token, "", mustParseURL(t, upstream.URL), "sk-real-key", "deepseek-v4-pro", "openai")
122
123 req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://127.0.0.1:%d/v1/chat/completions", port), strings.NewReader(`{"model":"deepseek-v4-flash","messages":[]}`))
124 if err != nil {
125 t.Fatal(err)
126 }
127 req.Header.Set("Authorization", "Bearer virtual-tok")
128 req.Header.Set("Content-Type", "application/json")
129 resp, err := http.DefaultClient.Do(req)
130 if err != nil {
131 t.Fatal(err)
132 }
133 defer resp.Body.Close()
134 if resp.StatusCode != http.StatusOK {
135 t.Fatalf("status = %d", resp.StatusCode)
136 }
137 if !strings.Contains(gotBody, `"model":"deepseek-v4-pro"`) {
138 t.Fatalf("upstream body = %q, want rewritten model deepseek-v4-pro", gotBody)
139 }
140 if strings.Contains(gotBody, "deepseek-v4-flash") {
141 t.Fatalf("upstream still saw the serve's model: %q", gotBody)
142 }
143 if want := strings.TrimPrefix(strings.TrimPrefix(upstream.URL, "http://"), "http://"); gotHost != want {
144 t.Fatalf("upstream Host = %q, want the upstream's own host %q", gotHost, want)
145 }
146 }
147
148 func TestCredentialProxyRejectsUnreadableOrOversizeBodies(t *testing.T) {
149 upstreamCalls := 0
150 upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
151 upstreamCalls++
152 }))
153 defer upstream.Close()
154 p := &credentialProxy{routes: map[string]*credProxyRoute{}}
155 p.setRoute("virtual-tok", "", mustParseURL(t, upstream.URL), "sk-real-key", "model", "openai")
156
157 request := func(body io.ReadCloser, contentLength int64) int {
158 req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1/v1/chat/completions", body)
159 req.Header.Set("Authorization", "Bearer virtual-tok")
160 req.ContentLength = contentLength
161 recorder := httptest.NewRecorder()
162 p.ServeHTTP(recorder, req)
163 return recorder.Code
164 }
165 if code := request(failingRequestBody{}, -1); code != http.StatusBadRequest {
166 t.Fatalf("unreadable body status = %d, want 400", code)
167 }
168 if code := request(io.NopCloser(strings.NewReader("{}")), (64<<20)+1); code != http.StatusRequestEntityTooLarge {
169 t.Fatalf("oversize body status = %d, want 413", code)
170 }
171 if upstreamCalls != 0 {
172 t.Fatalf("invalid bodies reached upstream %d times", upstreamCalls)
173 }
174 }
175
176 // TestCredentialProxyTokenStableAcrossRestarts: the virtual token derives
177 // from a persisted secret plus host/workspace/model identity, so a restarted
178 // desktop keeps the same route while distinct workspaces stay isolated.
179 func TestCredentialProxyTokenStableAcrossRestarts(t *testing.T) {
180 seedBridgeTestHost(t, "box")
181 const keyEnv = "TEST_PROXY_TOKEN_STABILITY_KEY"
182 setDesktopTestCredential(t, keyEnv, "sk-test")
183 configureCredentialProxyTestModels(t, "https://example.invalid/v1", keyEnv)
184 a1 := &App{}
185 t.Cleanup(a1.closeCredentialProxy)
186 i1, err := a1.registerCredentialProxyRoute("box", "~/app")
187 if err != nil {
188 t.Fatal(err)
189 }
190 a2 := &App{}
191 t.Cleanup(a2.closeCredentialProxy)
192 i2, err := a2.registerCredentialProxyRoute("box", "~/app")
193 if err != nil {
194 t.Fatal(err)
195 }
196 if i1.token == "" || i1.token != i2.token {
197 t.Fatalf("token drifted across App instances: %q vs %q", i1.token, i2.token)
198 }
199 i3, err := a2.registerCredentialProxyRoute("other", "~/app")
200 if err != nil {
201 t.Fatal(err)
202 }
203 if i3.token == i1.token {
204 t.Fatalf("different hosts share a token: %q", i1.token)
205 }
206 i4, err := a2.registerCredentialProxyRoute("box", "~/other")
207 if err != nil {
208 t.Fatal(err)
209 }
210 if i4.token == i1.token {
211 t.Fatalf("different workspaces share a token: %q", i1.token)
212 }
213 }
214
215 func TestCredentialProxyModelTokensKeepRoutesImmutable(t *testing.T) {
216 secret := strings.Repeat("ab", 32)
217 one := credentialProxyModelTokenFor(secret, "box", "~/app", "provider/model-a")
218 two := credentialProxyModelTokenFor(secret, "box", "~/app", "provider/model-b")
219 again := credentialProxyModelTokenFor(secret, "box", "~/app", "provider/model-a")
220 if one == two {
221 t.Fatal("different models shared one mutable credential proxy route token")
222 }
223 if one != again {
224 t.Fatal("the same model route token was not stable across registration")
225 }
226 if collision := credentialProxyModelTokenFor(secret, "box", "~/app", "provider:model-a"); collision == credentialProxyModelTokenFor(secret, "box", "~/app:provider", "model-a") {
227 t.Fatal("length-framed route identities shared a token")
228 }
229 }
230
231 func TestCredentialProxyReconnectRegistersTrackedWorkspaces(t *testing.T) {
232 seedBridgeTestHost(t, "box")
233 const keyEnv = "TEST_PROXY_RECONNECT_KEY"
234 setDesktopTestCredential(t, keyEnv, "sk-test")
235 configureCredentialProxyTestModels(t, "https://example.invalid/v1", keyEnv)
236 app := &App{}
237 t.Cleanup(app.closeCredentialProxy)
238 mgr := newDesktopRemoteManager(app)
239 mgr.hosts["box"] = &managedHost{serves: map[string]*serveEntry{
240 "~/app": {},
241 "~/other": {},
242 }}
243 info, err := mgr.registerTrackedCredentialRoutes(app, "box", "~/app")
244 if err != nil {
245 t.Fatal(err)
246 }
247 cfg, err := config.Load()
248 if err != nil {
249 t.Fatal(err)
250 }
251 other, err := app.applyCredentialProxyModel("box", "~/other", cfg.DefaultModel)
252 if err != nil {
253 t.Fatal(err)
254 }
255 app.credProxy.mu.Lock()
256 defer app.credProxy.mu.Unlock()
257 if info.token == "" || app.credProxy.routes[info.token] == nil || app.credProxy.routes[other.token] == nil {
258 t.Fatalf("tracked routes were not registered together: current=%q count=%d", info.token, len(app.credProxy.routes))
259 }
260 }
261
262 func TestCredentialWatchdogHealsEveryTrackedWorkspace(t *testing.T) {
263 mgr := newDesktopRemoteManager(nil)
264 mgr.hosts["box"] = &managedHost{serves: map[string]*serveEntry{
265 "~/alpha": {},
266 "~/beta": {},
267 "~/gamma": {},
268 }}
269 workspaces := mgr.trackedCredentialWorkspaces("box", "~/beta")
270 want := []string{"~/beta", "~/alpha", "~/gamma"}
271 if !slices.Equal(workspaces, want) {
272 t.Fatalf("tracked credential workspaces = %v, want %v", workspaces, want)
273 }
274
275 var setupCalls, healCalls []string
276 err := healTrackedCredentialProviders(context.Background(), workspaces,
277 func(workspace string) (*bootstrap.CredentialProxyOptions, error) {
278 setupCalls = append(setupCalls, workspace)
279 return &bootstrap.CredentialProxyOptions{Provider: workspace}, nil
280 },
281 func(_ context.Context, opts *bootstrap.CredentialProxyOptions) error {
282 healCalls = append(healCalls, opts.Provider)
283 return nil
284 },
285 )
286 if err != nil {
287 t.Fatal(err)
288 }
289 if !slices.Equal(setupCalls, want) || !slices.Equal(healCalls, want) {
290 t.Fatalf("credential heals = setup:%v heal:%v, want every workspace %v", setupCalls, healCalls, want)
291 }
292 }
293
294 func TestCredentialEnsureHealsEveryConfigBeforeReload(t *testing.T) {
295 workspaces := []string{"~/current", "~/peer"}
296 var calls []string
297 err := healCredentialConfigsBeforeReload(t.Context(), workspaces,
298 func(workspace string) (*bootstrap.CredentialProxyOptions, error) {
299 calls = append(calls, "setup:"+workspace)
300 return &bootstrap.CredentialProxyOptions{Provider: workspace}, nil
301 },
302 func(_ context.Context, opts *bootstrap.CredentialProxyOptions) error {
303 calls = append(calls, "heal:"+opts.Provider)
304 return nil
305 },
306 func() bool {
307 calls = append(calls, "reload")
308 return true
309 },
310 )
311 if err != nil {
312 t.Fatal(err)
313 }
314 want := []string{"setup:~/current", "heal:~/current", "setup:~/peer", "heal:~/peer", "reload"}
315 if !slices.Equal(calls, want) {
316 t.Fatalf("ensure heal order = %v, want %v", calls, want)
317 }
318 }
319
320 func TestEnsureServerRejectsRemovedHost(t *testing.T) {
321 home := t.TempDir()
322 t.Setenv("REASONIX_HOME", home)
323 t.Setenv("HOME", home)
324 client := newLifecycleSSHClient(nil)
325 mgr := newDesktopRemoteManager(nil)
326 ctx, cancel := context.WithCancel(context.Background())
327 t.Cleanup(cancel)
328 mgr.hosts["removed"] = &managedHost{ctx: ctx, cancel: cancel, client: client, serves: map[string]*serveEntry{}}
329 called := false
330 mgr.ensureServe = func(context.Context, bootstrap.Conn, bootstrap.Options) (bootstrap.Result, error) {
331 called = true
332 return bootstrap.Result{}, nil
333 }
334 if _, _, err := mgr.EnsureServer(context.Background(), "removed", "~/app"); err == nil || !strings.Contains(err.Error(), "no longer configured") {
335 t.Fatalf("EnsureServer removed host error = %v", err)
336 }
337 if called {
338 t.Fatal("removed host reached remote bootstrap")
339 }
340 }
341
342 // TestCredentialProxyAnthropicAuthShape: an anthropic-kind route swaps the
343 // virtual token for x-api-key (+ anthropic-version) instead of a bearer
344 // header.
345 func TestCredentialProxyAnthropicAuthShape(t *testing.T) {
346 var gotKey, gotVersion, gotAuth string
347 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
348 gotKey = r.Header.Get("x-api-key")
349 gotVersion = r.Header.Get("anthropic-version")
350 gotAuth = r.Header.Get("Authorization")
351 _, _ = w.Write([]byte("ok"))
352 }))
353 defer upstream.Close()
354
355 seedBridgeTestHost(t, "box")
356 a := &App{}
357 t.Cleanup(a.closeCredentialProxy)
358 port, err := a.credentialProxyPort()
359 if err != nil {
360 t.Fatal(err)
361 }
362 a.credProxy.setRoute("virtual-tok", "", mustParseURL(t, upstream.URL), "sk-real-key", "", "anthropic")
363 req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://127.0.0.1:%d/v1/messages", port), strings.NewReader("{}"))
364 if err != nil {
365 t.Fatal(err)
366 }
367 req.Header.Set("Authorization", "Bearer virtual-tok")
368 resp, err := http.DefaultClient.Do(req)
369 if err != nil {
370 t.Fatal(err)
371 }
372 defer resp.Body.Close()
373 if resp.StatusCode != http.StatusOK {
374 t.Fatalf("status = %d", resp.StatusCode)
375 }
376 if gotKey != "sk-real-key" {
377 t.Fatalf("x-api-key = %q, want the real key", gotKey)
378 }
379 if gotVersion == "" {
380 t.Fatal("anthropic-version header missing")
381 }
382 if gotAuth != "" {
383 t.Fatalf("Authorization header leaked to the anthropic upstream: %q", gotAuth)
384 }
385 }
386
387 // TestRewriteJSONModelGuards: a literal null body must pass through without
388 // panicking (assigning into a nil map would), and a non-JSON body stays
389 // untouched.
390 func TestRewriteJSONModelGuards(t *testing.T) {
391 if got := rewriteJSONModel([]byte("null"), "m"); string(got) != "null" {
392 t.Fatalf("null body rewritten: %q", got)
393 }
394 if got := rewriteJSONModel([]byte("not json"), "m"); string(got) != "not json" {
395 t.Fatalf("non-JSON body rewritten: %q", got)
396 }
397 if got := rewriteJSONModel([]byte(`{"model":"a"}`), ""); string(got) != `{"model":"a"}` {
398 t.Fatalf("empty model rewrote the body: %q", got)
399 }
400 if got := rewriteJSONModel([]byte(`{"model":"a"}`), "b"); !strings.Contains(string(got), `"model":"b"`) {
401 t.Fatalf("model not rewritten: %q", got)
402 }
403 }
404
405 // TestCredentialModeConfigRoundTrip pins the host entry field end to end.
406 func TestCredentialModeConfigRoundTrip(t *testing.T) {
407 home := t.TempDir()
408 t.Setenv("REASONIX_HOME", home)
409 t.Setenv("HOME", home)
410 if err := editUserConfig(func(c *config.Config) error {
411 return c.UpsertRemoteHost(config.RemoteHostEntry{
412 Name: "p", Host: "127.0.0.1", CredentialMode: "local-proxy",
413 })
414 }); err != nil {
415 t.Fatal(err)
416 }
417 cfg, err := config.Load()
418 if err != nil {
419 t.Fatal(err)
420 }
421 entry, ok := cfg.RemoteHost("p")
422 if !ok || !entry.CredentialProxyEnabled() {
423 t.Fatalf("credential mode did not round-trip: %+v", entry)
424 }
425 if v := credentialModeView(entry); v != "local-proxy" {
426 t.Fatalf("view mode = %q", v)
427 }
428 if n := normalizeCredentialMode("bogus"); n != "" {
429 t.Fatalf("bogus mode normalized to %q", n)
430 }
431 }
432
433 // Detached/background controllers retain the connection captured at admission.
434 func TestSaveProviderCredentialPreservesEveryOldModelRoute(t *testing.T) {
435 isolateDesktopUserDirs(t)
436 const keyEnv = "TEST_PROXY_REFRESH_KEY"
437 setDesktopTestCredential(t, keyEnv, "sk-before-rotation")
438 auth := make(chan string, 2)
439 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
440 auth <- r.Header.Get("Authorization")
441 _, _ = w.Write([]byte("ok"))
442 }))
443 defer upstream.Close()
444
445 firstRef, secondRef := configureCredentialProxyTestModels(t, upstream.URL, keyEnv)
446 a := &App{}
447 t.Cleanup(a.closeCredentialProxy)
448 first, err := a.applyCredentialProxyModel("box", "~/app", firstRef)
449 if err != nil {
450 t.Fatal(err)
451 }
452 second, err := a.applyCredentialProxyModel("box", "~/app", secondRef)
453 if err != nil {
454 t.Fatal(err)
455 }
456 if first.token == second.token {
457 t.Fatal("different models shared one route token")
458 }
459 if _, err := a.saveProviderCredential(keyEnv, "sk-after-rotation"); err != nil {
460 t.Fatal(err)
461 }
462 for _, route := range []credentialProxyRouteInfo{first, second} {
463 requestCredentialProxy(t, route.port, route.token)
464 if got := <-auth; got != "Bearer sk-before-rotation" {
465 t.Fatalf("old route changed credential: %q", got)
466 }
467 }
468 latest, err := a.applyCredentialProxyModel("box", "~/app", firstRef)
469 if err != nil {
470 t.Fatal(err)
471 }
472 if latest.token == first.token {
473 t.Fatal("rotated credential reused an old token")
474 }
475 requestCredentialProxy(t, latest.port, latest.token)
476 if got := <-auth; got != "Bearer sk-after-rotation" {
477 t.Fatalf("new route credential: %q", got)
478 }
479 }
480
481 // Registration is serialized and a published token remains immutable even if
482 // a competing caller tries to reuse it with a different connection.
483 func TestCredentialProxyRejectsRouteReplacementDuringRegistration(t *testing.T) {
484 auth := make(chan string, 1)
485 upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
486 auth <- r.Header.Get("Authorization")
487 _, _ = w.Write([]byte("ok"))
488 }))
489 defer upstream.Close()
490 parsed := mustParseURL(t, upstream.URL)
491 p := &credentialProxy{routes: map[string]*credProxyRoute{}}
492
493 oldResolving := make(chan struct{})
494 releaseOld := make(chan struct{})
495 newCalling := make(chan struct{})
496 errs := make(chan error, 2)
497 var updates sync.WaitGroup
498 updates.Add(2)
499 go func() {
500 defer updates.Done()
501 _, err := p.resolveAndSetRoute("virtual-tok", "provider/model", func() (proxyUpstream, error) {
502 close(oldResolving)
503 <-releaseOld
504 return proxyUpstream{url: parsed, apiKey: "sk-older", model: "model", kind: "openai"}, nil
505 })
506 errs <- err
507 }()
508 <-oldResolving
509 go func() {
510 defer updates.Done()
511 close(newCalling)
512 _, err := p.resolveAndSetRoute("virtual-tok", "provider/model", func() (proxyUpstream, error) {
513 return proxyUpstream{url: parsed, apiKey: "sk-newer", model: "model", kind: "openai"}, nil
514 })
515 errs <- err
516 }()
517 <-newCalling
518 close(releaseOld)
519 updates.Wait()
520 close(errs)
521 for err := range errs {
522 if err != nil {
523 t.Fatal(err)
524 }
525 }
526
527 req := httptest.NewRequest(http.MethodPost, "http://127.0.0.1/v1/chat/completions", strings.NewReader(`{"model":"model"}`))
528 req.Header.Set("Authorization", "Bearer virtual-tok")
529 recorder := httptest.NewRecorder()
530 p.ServeHTTP(recorder, req)
531 if recorder.Code != http.StatusOK {
532 t.Fatalf("status = %d, want 200", recorder.Code)
533 }
534 if got := <-auth; got != "Bearer sk-older" {
535 t.Fatalf("published token changed credential: %q", got)
536 }
537 }
538
539 func configureCredentialProxyTestModels(t *testing.T, baseURL, keyEnv string) (string, string) {
540 t.Helper()
541 const firstRef = "proxy-first/model-a"
542 const secondRef = "proxy-second/model-b"
543 cfg := config.Default()
544 cfg.DefaultModel = firstRef
545 cfg.Providers = []config.ProviderEntry{
546 {Name: "proxy-first", Kind: "openai", BaseURL: baseURL, Model: "model-a", APIKeyEnv: keyEnv},
547 {Name: "proxy-second", Kind: "openai", BaseURL: baseURL, Model: "model-b", APIKeyEnv: keyEnv},
548 }
549 if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
550 t.Fatalf("save credential proxy test config: %v", err)
551 }
552 return firstRef, secondRef
553 }
554
555 func requestCredentialProxy(t *testing.T, port int, token string) {
556 t.Helper()
557 req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://127.0.0.1:%d/v1/chat/completions", port), strings.NewReader(`{"model":"placeholder"}`))
558 if err != nil {
559 t.Fatal(err)
560 }
561 req.Header.Set("Authorization", "Bearer "+token)
562 resp, err := http.DefaultClient.Do(req)
563 if err != nil {
564 t.Fatal(err)
565 }
566 defer resp.Body.Close()
567 if resp.StatusCode != http.StatusOK {
568 t.Fatalf("status = %d, want 200", resp.StatusCode)
569 }
570 }
571
571 lines GO