summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/authkeys/store_test.go77
-rw-r--r--internal/cli/cli_test.go32
-rw-r--r--internal/daemon/daemon_test.go244
-rw-r--r--internal/daemon/upload_test.go239
4 files changed, 519 insertions, 73 deletions
diff --git a/internal/authkeys/store_test.go b/internal/authkeys/store_test.go
index 8331a5b..b8da623 100644
--- a/internal/authkeys/store_test.go
+++ b/internal/authkeys/store_test.go
@@ -3,6 +3,7 @@ package authkeys
import (
"context"
"path/filepath"
+ "strings"
"testing"
)
@@ -63,3 +64,79 @@ func TestDefaultPath(t *testing.T) {
t.Fatalf("got %q", p)
}
}
+
+func TestCloseNilStore(t *testing.T) {
+ var s *Store
+ if err := s.Close(); err != nil {
+ t.Fatalf("Close nil: %v", err)
+ }
+}
+
+func TestCloseNilDB(t *testing.T) {
+ s := &Store{}
+ if err := s.Close(); err != nil {
+ t.Fatalf("Close nil db: %v", err)
+ }
+}
+
+func TestCreateKeyEmptyHostname(t *testing.T) {
+ ctx := context.Background()
+ path := filepath.Join(t.TempDir(), "auth.db")
+ s, err := OpenStore(ctx, path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+ if err := s.EnsureSchema(ctx); err != nil {
+ t.Fatal(err)
+ }
+ _, err = s.CreateKey(ctx, "")
+ if err == nil || !strings.Contains(err.Error(), "hostname") {
+ t.Fatalf("expected empty hostname error, got %v", err)
+ }
+}
+
+func TestVerifyUnknownHost(t *testing.T) {
+ ctx := context.Background()
+ path := filepath.Join(t.TempDir(), "auth.db")
+ s, err := OpenStore(ctx, path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer s.Close()
+ if err := s.EnsureSchema(ctx); err != nil {
+ t.Fatal(err)
+ }
+ ok, err := s.Verify(ctx, "nohost", "any")
+ if err != nil || ok {
+ t.Fatalf("Verify unknown host: ok=%v err=%v", ok, err)
+ }
+}
+
+func TestOpsAfterClose(t *testing.T) {
+ ctx := context.Background()
+ path := filepath.Join(t.TempDir(), "auth.db")
+ s, err := OpenStore(ctx, path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := s.EnsureSchema(ctx); err != nil {
+ s.Close()
+ t.Fatal(err)
+ }
+ if _, err := s.CreateKey(ctx, "h"); err != nil {
+ s.Close()
+ t.Fatal(err)
+ }
+ if err := s.Close(); err != nil {
+ t.Fatal(err)
+ }
+ _, err = s.KeyCount(ctx)
+ if err == nil {
+ t.Fatal("KeyCount after close expected error")
+ }
+ _, err = s.Verify(ctx, "h", "x")
+ if err == nil {
+ t.Fatal("Verify after close expected error")
+ }
+}
diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go
index ff7b046..a89fa50 100644
--- a/internal/cli/cli_test.go
+++ b/internal/cli/cli_test.go
@@ -118,7 +118,14 @@ func TestStableImportAndQuery(t *testing.T) {
func TestStableIntegrationTestSubcommand(t *testing.T) {
root := moduleRoot(t)
- t.Chdir(root)
+ cwd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Chdir(root); err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = os.Chdir(cwd) }()
if err := Execute([]string{"test"}); err != nil {
t.Fatal(err)
}
@@ -174,3 +181,26 @@ func TestCreateClientKeyWritesToken(t *testing.T) {
t.Fatalf("token too short %q", tok)
}
}
+
+func TestDefaultListenFromEnv(t *testing.T) {
+ t.Setenv("GOPRECORDS_LISTEN", ":7777")
+ if defaultListenFromEnv() != ":7777" {
+ t.Fatalf("got %q", defaultListenFromEnv())
+ }
+ t.Setenv("GOPRECORDS_LISTEN", "")
+ if defaultListenFromEnv() != ":8080" {
+ t.Fatalf("default got %q", defaultListenFromEnv())
+ }
+}
+
+func TestCreateClientKeyWithAuthDBOnly(t *testing.T) {
+ db := filepath.Join(t.TempDir(), "keys.db")
+ out := captureStdout(t, func() {
+ if err := Execute([]string{"--create-client-key", "hostonly", "-auth-db", db}); err != nil {
+ t.Fatal(err)
+ }
+ })
+ if len(strings.TrimSpace(out)) < 20 {
+ t.Fatalf("token too short %q", out)
+ }
+}
diff --git a/internal/daemon/daemon_test.go b/internal/daemon/daemon_test.go
index e92359b..143ec3a 100644
--- a/internal/daemon/daemon_test.go
+++ b/internal/daemon/daemon_test.go
@@ -162,102 +162,125 @@ func TestReadyzAuthDBDirNotWritable(t *testing.T) {
}
}
-func TestReport(t *testing.T) {
- fixtures := filepath.Join("..", "..", "fixtures")
- h := Handler(fixtures)
- srv := httptest.NewServer(h)
- defer srv.Close()
- res, err := http.Get(srv.URL + "/report?category=Host&metric=Boots&limit=3&output-format=Plaintext")
- if err != nil {
- t.Fatal(err)
- }
- defer res.Body.Close()
- if res.StatusCode != http.StatusOK {
- t.Fatalf("status %d", res.StatusCode)
- }
- if ct := res.Header.Get("Content-Type"); ct != "text/plain; charset=utf-8" {
- t.Fatalf("Content-Type %q", ct)
- }
- b, err := io.ReadAll(res.Body)
- if err != nil {
- t.Fatal(err)
- }
- if !strings.Contains(string(b), "Host") {
- t.Fatalf("expected report body, got %q", b)
- }
-}
-
-func TestReportQueryAliases(t *testing.T) {
+func TestReportHTTPTable(t *testing.T) {
fixtures := filepath.Join("..", "..", "fixtures")
srv := httptest.NewServer(Handler(fixtures))
defer srv.Close()
- res, err := http.Get(srv.URL + "/report?Category=Host&Metric=Uptime&limit=2&OutputFormat=Markdown")
- if err != nil {
- t.Fatal(err)
- }
- defer res.Body.Close()
- if res.StatusCode != http.StatusOK {
- t.Fatalf("status %d", res.StatusCode)
- }
- if ct := res.Header.Get("Content-Type"); ct != "text/markdown; charset=utf-8" {
- t.Fatalf("Content-Type %q", ct)
- }
- b, _ := io.ReadAll(res.Body)
- body := string(b)
- if !strings.Contains(body, "# Top") || !strings.Contains(body, "```") {
- t.Fatalf("expected markdown heading and fence, got %q", b)
+ tests := []struct {
+ name string
+ query string
+ wantCode int
+ wantCTPfx string
+ bodyNeedle []string
+ }{
+ {
+ name: "plaintext",
+ query: "category=Host&metric=Boots&limit=3&output-format=Plaintext",
+ wantCode: http.StatusOK,
+ wantCTPfx: "text/plain",
+ bodyNeedle: []string{"Host"},
+ },
+ {
+ name: "markdown aliases",
+ query: "Category=Host&Metric=Uptime&limit=2&OutputFormat=Markdown",
+ wantCode: http.StatusOK,
+ wantCTPfx: "text/markdown",
+ bodyNeedle: []string{"# Top", "```"},
+ },
+ {
+ name: "html",
+ query: "OutputFormat=HTML&limit=2",
+ wantCode: http.StatusOK,
+ wantCTPfx: "text/html",
+ bodyNeedle: []string{"<!DOCTYPE html>", "<pre>"},
+ },
+ {
+ name: "gemtext",
+ query: "output-format=Gemtext&limit=2",
+ wantCode: http.StatusOK,
+ wantCTPfx: "text/gemini",
+ },
+ {
+ name: "bad category",
+ query: "category=Nope",
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ name: "invalid limit",
+ query: "limit=notnum",
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ name: "invalid all bool",
+ query: "all=nope",
+ wantCode: http.StatusBadRequest,
+ },
+ {
+ name: "downtime on non host",
+ query: "category=Kernel&metric=Downtime&limit=2",
+ wantCode: http.StatusBadRequest,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ res, err := http.Get(srv.URL + "/report?" + tt.query)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer res.Body.Close()
+ if res.StatusCode != tt.wantCode {
+ t.Fatalf("status %d want %d", res.StatusCode, tt.wantCode)
+ }
+ if tt.wantCTPfx != "" {
+ ct := res.Header.Get("Content-Type")
+ if !strings.HasPrefix(ct, tt.wantCTPfx) {
+ t.Fatalf("Content-Type %q want prefix %q", ct, tt.wantCTPfx)
+ }
+ }
+ b, _ := io.ReadAll(res.Body)
+ body := string(b)
+ for _, sub := range tt.bodyNeedle {
+ if !strings.Contains(body, sub) {
+ t.Fatalf("body missing %q: %q", sub, body)
+ }
+ }
+ })
}
}
-func TestReportHTMLContentType(t *testing.T) {
+func TestReportMethodNotAllowed(t *testing.T) {
fixtures := filepath.Join("..", "..", "fixtures")
srv := httptest.NewServer(Handler(fixtures))
defer srv.Close()
- res, err := http.Get(srv.URL + "/report?OutputFormat=HTML&limit=2")
+ req, _ := http.NewRequest(http.MethodPost, srv.URL+"/report?limit=2", nil)
+ res, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
- defer res.Body.Close()
- if res.StatusCode != http.StatusOK {
+ res.Body.Close()
+ if res.StatusCode != http.StatusMethodNotAllowed {
t.Fatalf("status %d", res.StatusCode)
}
- if ct := res.Header.Get("Content-Type"); ct != "text/html; charset=utf-8" {
- t.Fatalf("Content-Type %q", ct)
- }
- b, _ := io.ReadAll(res.Body)
- body := string(b)
- if !strings.Contains(body, "<!DOCTYPE html>") || !strings.Contains(body, "<pre>") {
- t.Fatalf("expected HTML body, got %q", body)
- }
}
-func TestReportGemtextContentType(t *testing.T) {
- fixtures := filepath.Join("..", "..", "fixtures")
- srv := httptest.NewServer(Handler(fixtures))
- defer srv.Close()
- res, err := http.Get(srv.URL + "/report?output-format=Gemtext&limit=2")
- if err != nil {
+func TestReportAggregateFailure(t *testing.T) {
+ dir := t.TempDir()
+ line := "1:1:Linux 5.13.14-200.fc34.x86_64\n"
+ if err := os.WriteFile(filepath.Join(dir, "dup.x.records"), []byte(line), 0o644); err != nil {
t.Fatal(err)
}
- defer res.Body.Close()
- if res.StatusCode != http.StatusOK {
- t.Fatalf("status %d", res.StatusCode)
- }
- if ct := res.Header.Get("Content-Type"); ct != "text/gemini; charset=utf-8" {
- t.Fatalf("Content-Type %q", ct)
+ if err := os.WriteFile(filepath.Join(dir, "dup.y.records"), []byte(line), 0o644); err != nil {
+ t.Fatal(err)
}
-}
-
-func TestReportBadQuery(t *testing.T) {
- srv := httptest.NewServer(Handler(t.TempDir()))
+ srv := httptest.NewServer(Handler(dir))
defer srv.Close()
- res, err := http.Get(srv.URL + "/report?category=Nope")
+ res, err := http.Get(srv.URL + "/report?limit=2")
if err != nil {
t.Fatal(err)
}
res.Body.Close()
- if res.StatusCode != http.StatusBadRequest {
- t.Fatalf("status %d", res.StatusCode)
+ if res.StatusCode != http.StatusInternalServerError {
+ t.Fatalf("status %d want 500", res.StatusCode)
}
}
@@ -296,6 +319,83 @@ func TestRunWritesDaemonListenToLogOutput(t *testing.T) {
<-done
}
+func TestRunUsesStdoutWhenLogOutputNil(t *testing.T) {
+ old := os.Stdout
+ pr, pw, err := os.Pipe()
+ if err != nil {
+ t.Fatal(err)
+ }
+ os.Stdout = pw
+ var logBuf bytes.Buffer
+ copyDone := make(chan struct{})
+ go func() {
+ _, _ = io.Copy(&logBuf, pr)
+ close(copyDone)
+ }()
+ ctx, cancel := context.WithCancel(context.Background())
+ cfg := Config{StatsDir: t.TempDir(), Addr: "127.0.0.1:0", LogOutput: nil}
+ runDone := make(chan struct{})
+ go func() {
+ _ = Run(ctx, cfg)
+ close(runDone)
+ }()
+ deadline := time.After(2 * time.Second)
+ for !strings.Contains(logBuf.String(), "daemon_listen") {
+ select {
+ case <-deadline:
+ cancel()
+ _ = pw.Close()
+ <-runDone
+ <-copyDone
+ _ = pr.Close()
+ os.Stdout = old
+ t.Fatalf("timeout, got %q", logBuf.String())
+ case <-time.After(5 * time.Millisecond):
+ }
+ }
+ cancel()
+ <-runDone
+ os.Stdout = old
+ if err := pw.Close(); err != nil {
+ t.Fatal(err)
+ }
+ <-copyDone
+ if err := pr.Close(); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRunInvalidListenAddress(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
+ defer cancel()
+ err := Run(ctx, Config{StatsDir: t.TempDir(), Addr: ":999999999"})
+ if err == nil {
+ t.Fatal("expected listen error")
+ }
+ if !strings.Contains(err.Error(), "listen") {
+ t.Fatalf("expected listen in error: %v", err)
+ }
+}
+
+func TestAccessLogImplicitOKStatus(t *testing.T) {
+ var buf bytes.Buffer
+ log := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo}))
+ mux := http.NewServeMux()
+ mux.HandleFunc("/nohdr", func(w http.ResponseWriter, r *http.Request) {
+ _, _ = w.Write([]byte("ok"))
+ })
+ srv := httptest.NewServer(withAccessLog(log, mux))
+ defer srv.Close()
+ res, err := http.Get(srv.URL + "/nohdr")
+ if err != nil {
+ t.Fatal(err)
+ }
+ res.Body.Close()
+ if !strings.Contains(buf.String(), "status=200") {
+ t.Fatalf("log %q", buf.String())
+ }
+}
+
func TestAccessLogLineToWriter(t *testing.T) {
var buf bytes.Buffer
h := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})
diff --git a/internal/daemon/upload_test.go b/internal/daemon/upload_test.go
new file mode 100644
index 0000000..5a3755f
--- /dev/null
+++ b/internal/daemon/upload_test.go
@@ -0,0 +1,239 @@
+package daemon
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "testing/iotest"
+)
+
+func TestParseBearer(t *testing.T) {
+ tests := []struct {
+ name string
+ header string
+ wantTok string
+ wantOK bool
+ }{
+ {name: "valid", header: "Bearer abc.def", wantTok: "abc.def", wantOK: true},
+ {name: "valid case", header: "bearer xyz", wantTok: "xyz", wantOK: true},
+ {name: "extra space", header: "Bearer tok ", wantTok: "tok", wantOK: true},
+ {name: "empty", header: "", wantOK: false},
+ {name: "whitespace only", header: " ", wantOK: false},
+ {name: "too short", header: "Bear", wantOK: false},
+ {name: "wrong scheme", header: "Basic xxx", wantOK: false},
+ {name: "bearer no token", header: "Bearer ", wantOK: false},
+ {name: "bearer only spaces", header: "Bearer ", wantOK: false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tok, ok := parseBearer(tt.header)
+ if ok != tt.wantOK || tok != tt.wantTok {
+ t.Fatalf("parseBearer(%q) = (%q, %v) want (%q, %v)", tt.header, tok, ok, tt.wantTok, tt.wantOK)
+ }
+ })
+ }
+}
+
+func TestParseUploadPath(t *testing.T) {
+ tests := []struct {
+ name string
+ path string
+ wantHost string
+ wantKind string
+ wantOK bool
+ }{
+ {name: "ok txt", path: "/upload/host1/txt", wantHost: "host1", wantKind: "txt", wantOK: true},
+ {name: "ok nested kind", path: "/upload/my-host/cur.txt", wantHost: "my-host", wantKind: "cur.txt", wantOK: true},
+ {name: "no prefix", path: "/x/upload/h/k", wantOK: false},
+ {name: "empty rest", path: "/upload/", wantOK: false},
+ {name: "dotdot", path: "/upload/../x/txt", wantOK: false},
+ {name: "no slash", path: "/upload/only", wantOK: false},
+ {name: "slash end", path: "/upload/host/", wantOK: false},
+ {name: "bad host", path: "/upload/bad host/txt", wantOK: false},
+ {name: "kind slash", path: "/upload/h/a/b", wantOK: false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h, k, ok := parseUploadPath(tt.path)
+ if ok != tt.wantOK {
+ t.Fatalf("ok=%v want %v (host=%q kind=%q)", ok, tt.wantOK, h, k)
+ }
+ if tt.wantOK && (h != tt.wantHost || k != tt.wantKind) {
+ t.Fatalf("host=%q kind=%q want %q %q", h, k, tt.wantHost, tt.wantKind)
+ }
+ })
+ }
+}
+
+func TestSafeHostSegment(t *testing.T) {
+ tests := []struct {
+ s string
+ want bool
+ }{
+ {"a", true},
+ {"Host-1._x", true},
+ {"", false},
+ {"bad space", false},
+ {"bad:colon", false},
+ {strings.Repeat("a", 254), false},
+ {strings.Repeat("a", 253), true},
+ }
+ for _, tt := range tests {
+ if got := safeHostSegment(tt.s); got != tt.want {
+ t.Fatalf("safeHostSegment(%q)=%v want %v", tt.s, got, tt.want)
+ }
+ }
+}
+
+func TestFileUnderDir(t *testing.T) {
+ dir := t.TempDir()
+ inside := filepath.Join(dir, "f.txt")
+ if err := os.WriteFile(inside, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ outside := filepath.Join(t.TempDir(), "escape.txt")
+ if err := os.WriteFile(outside, []byte("y"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ tests := []struct {
+ name string
+ dir string
+ file string
+ want bool
+ }{
+ {"inside", dir, inside, true},
+ {"same as dir", dir, dir, false},
+ {"parent escape", dir, outside, false},
+ {"dotdot file", dir, filepath.Join(dir, "..", filepath.Base(outside)), false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := fileUnderDir(tt.dir, tt.file); got != tt.want {
+ t.Fatalf("fileUnderDir(%q,%q)=%v want %v", tt.dir, tt.file, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestWriteUploadBodyTooLarge(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "big.txt")
+ body := bytes.Repeat([]byte{'z'}, maxUploadBytes+1)
+ err := writeUploadBody(path, bytes.NewReader(body))
+ if err == nil || !strings.Contains(err.Error(), "too large") {
+ t.Fatalf("expected body too large, got %v", err)
+ }
+ if _, err := os.Stat(path); err == nil {
+ t.Fatal("final file should not exist")
+ }
+}
+
+func TestWriteUploadBodyReadError(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "e.txt")
+ err := writeUploadBody(path, iotest.ErrReader(errors.New("read boom")))
+ if err == nil || !strings.Contains(err.Error(), "write") {
+ t.Fatalf("expected write wrap error, got %v", err)
+ }
+}
+
+func TestWriteUploadBodyCreateFails(t *testing.T) {
+ dir := t.TempDir()
+ if err := os.Chmod(dir, 0o555); err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = os.Chmod(dir, 0o755) }()
+ path := filepath.Join(dir, "nope.txt")
+ err := writeUploadBody(path, strings.NewReader("a"))
+ if err == nil || !strings.Contains(err.Error(), "create temp") {
+ t.Fatalf("expected create temp error, got %v", err)
+ }
+}
+
+func TestWriteUploadBodyRenameFails(t *testing.T) {
+ dir := t.TempDir()
+ target := filepath.Join(dir, "block")
+ if err := os.Mkdir(target, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ path := filepath.Join(dir, "block")
+ err := writeUploadBody(path, strings.NewReader("x"))
+ if err == nil || !strings.Contains(err.Error(), "rename") {
+ t.Fatalf("expected rename error, got %v", err)
+ }
+}
+
+func TestOpenAuthStoreBadPath(t *testing.T) {
+ ctx := context.Background()
+ _, err := openAuthStore(ctx, t.TempDir(), "/dev/null/impossible/goprecords-auth.db")
+ if err == nil {
+ t.Fatal("expected error opening auth store under /dev/null")
+ }
+}
+
+func TestUploadMethodNotAllowedTable(t *testing.T) {
+ statsDir := t.TempDir()
+ srv := httptest.NewServer(Handler(statsDir))
+ defer srv.Close()
+ for _, method := range []string{http.MethodGet, http.MethodPost, http.MethodDelete} {
+ t.Run(method, func(t *testing.T) {
+ req, _ := http.NewRequest(method, srv.URL+"/upload/h/txt", nil)
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res.Body.Close()
+ if res.StatusCode != http.StatusMethodNotAllowed {
+ t.Fatalf("status %d", res.StatusCode)
+ }
+ })
+ }
+}
+
+func TestUploadAuthBearerNegativeTable(t *testing.T) {
+ statsDir := t.TempDir()
+ ctx := context.Background()
+ store, err := openAuthStore(ctx, statsDir, "")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer store.Close()
+ if _, err := store.CreateKey(ctx, "myhost"); err != nil {
+ t.Fatal(err)
+ }
+ srv := httptest.NewServer(routes(statsDir, "", store))
+ defer srv.Close()
+ url := srv.URL + "/upload/myhost/txt"
+ tests := []struct {
+ name string
+ hdr string
+ status int
+ }{
+ {"no auth", "", http.StatusUnauthorized},
+ {"basic", "Basic x", http.StatusUnauthorized},
+ {"bearer empty", "Bearer ", http.StatusUnauthorized},
+ {"wrong token", "Bearer not-the-token", http.StatusForbidden},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ req, _ := http.NewRequest(http.MethodPut, url, strings.NewReader("data"))
+ if tt.hdr != "" {
+ req.Header.Set("Authorization", tt.hdr)
+ }
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ res.Body.Close()
+ if res.StatusCode != tt.status {
+ t.Fatalf("status %d want %d", res.StatusCode, tt.status)
+ }
+ })
+ }
+}