433 lines
14 KiB
Go
433 lines
14 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"github.com/alexedwards/scs/v2"
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/auth"
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/billing"
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/config"
|
||
|
|
"github.com/descrybe/descrybe-v2/apps/api/internal/processing"
|
||
|
|
"github.com/google/uuid"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestHandleAdminDiagnosticsNilPool(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
dir := t.TempDir()
|
||
|
|
s := &Server{Config: config.Config{UploadDir: dir, AppEnv: "test"}}
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/api/admin/diagnostics", nil)
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
s.handleAdminDiagnostics(rec, req)
|
||
|
|
if rec.Code != http.StatusOK {
|
||
|
|
t.Fatalf("status=%d want 200 body=%s", rec.Code, rec.Body.String())
|
||
|
|
}
|
||
|
|
var body map[string]any
|
||
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||
|
|
t.Fatalf("json: %v", err)
|
||
|
|
}
|
||
|
|
if body["status"] != "fail" {
|
||
|
|
t.Fatalf("overall status=%v want fail", body["status"])
|
||
|
|
}
|
||
|
|
if _, ok := body["runtime_metrics"].(map[string]any); !ok {
|
||
|
|
t.Fatalf("expected runtime_metrics object, got %#v", body["runtime_metrics"])
|
||
|
|
}
|
||
|
|
cutover, ok := body["cutover"].(map[string]any)
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("expected cutover object, got %#v", body["cutover"])
|
||
|
|
}
|
||
|
|
if _, ok := cutover["goose"].(map[string]any); !ok {
|
||
|
|
t.Fatalf("expected cutover.goose object, got %#v", cutover["goose"])
|
||
|
|
}
|
||
|
|
if _, ok := cutover["worker"].(map[string]any); !ok {
|
||
|
|
t.Fatalf("expected cutover.worker object, got %#v", cutover["worker"])
|
||
|
|
}
|
||
|
|
if _, hasPlans := cutover["companies_without_plan"]; hasPlans {
|
||
|
|
t.Fatal("nil pool must omit companies_without_plan (query skipped)")
|
||
|
|
}
|
||
|
|
links, _ := body["links"].(map[string]any)
|
||
|
|
if links["metrics"] != "/metrics" {
|
||
|
|
t.Fatalf("links.metrics=%v want /metrics", links["metrics"])
|
||
|
|
}
|
||
|
|
if links["readiness"] != "/api/admin/readiness" {
|
||
|
|
t.Fatalf("links.readiness=%v want /api/admin/readiness", links["readiness"])
|
||
|
|
}
|
||
|
|
cfg, _ := body["config"].(map[string]any)
|
||
|
|
for _, secretKey := range []string{
|
||
|
|
"database_url", "token_signing_secret", "openai_api_key", "smtp_password",
|
||
|
|
"stripe_secret_key", "pinecone_api_key", "password",
|
||
|
|
} {
|
||
|
|
if _, ok := cfg[secretKey]; ok {
|
||
|
|
t.Fatalf("config must not expose %q", secretKey)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
raw := strings.ToLower(rec.Body.String())
|
||
|
|
for _, leak := range []string{"sk_live", "password=", "postgres://", "bearer "} {
|
||
|
|
if strings.Contains(raw, leak) {
|
||
|
|
t.Fatalf("response leaked secret-like substring %q", leak)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandleAdminDiagnosticsInvalidStatus(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
s := &Server{Config: config.Config{UploadDir: t.TempDir()}}
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/api/admin/diagnostics?status=bogus", nil)
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
s.handleAdminDiagnostics(rec, req)
|
||
|
|
if rec.Code != http.StatusBadRequest {
|
||
|
|
t.Fatalf("status=%d want 400 body=%s", rec.Code, rec.Body.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestDiagConfigSanityNeverEmitsSecretValues seeds Config with realistic secrets and
|
||
|
|
// asserts the diagnostics payload only exposes presence flags — never values/DSNs.
|
||
|
|
func TestDiagConfigSanityNeverEmitsSecretValues(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
s := &Server{
|
||
|
|
Config: config.Config{
|
||
|
|
AppEnv: "production",
|
||
|
|
UploadDir: t.TempDir(),
|
||
|
|
DatabaseURL: "postgres://descrybe:s3cret@localhost:5433/descrybe",
|
||
|
|
TokenSigningSecret: "super-secret-token-signing-key",
|
||
|
|
OpenAIAPIKey: "sk-abcdefghijklmnopqrstuvwxyz0123456789",
|
||
|
|
PineconeAPIKey: "pcsk_live_example_key_value",
|
||
|
|
StripeSecretKey: "sk_live_51ExampleSecretValue",
|
||
|
|
StripeWebhookSecret: "whsec_example_webhook_secret",
|
||
|
|
SMTPPassword: "smtp-password-value",
|
||
|
|
SMTPHost: "smtp.secret-host.example",
|
||
|
|
SMTPEnabled: true,
|
||
|
|
EmailDryRun: true,
|
||
|
|
CredentialsEncryptionKey: "creds-encryption-key-32bytes!!",
|
||
|
|
ResendAPIKey: "re_example_resend_key",
|
||
|
|
EPRELAPIKey: "eprel-secret-key",
|
||
|
|
WebOrigin: "https://app.example.com",
|
||
|
|
PublicAPIURL: "https://api.example.com",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
stripeCfg := billing.StripeConfig{
|
||
|
|
SecretKey: s.Config.StripeSecretKey,
|
||
|
|
WebhookSecret: s.Config.StripeWebhookSecret,
|
||
|
|
ForceMock: s.Config.StripeMock,
|
||
|
|
}
|
||
|
|
cfg := s.diagConfigSanity(stripeCfg)
|
||
|
|
raw, err := json.Marshal(cfg)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
body := strings.ToLower(string(raw))
|
||
|
|
for _, leak := range []string{
|
||
|
|
"postgres://", "s3cret", "super-secret-token",
|
||
|
|
"sk-abcdefghijklmnopqrstuvwxyz", "sk_live_51", "whsec_",
|
||
|
|
"smtp-password", "creds-encryption", "re_example", "eprel-secret",
|
||
|
|
"database_url", "openai_api_key", "smtp_password", "smtp.secret-host",
|
||
|
|
} {
|
||
|
|
if strings.Contains(body, strings.ToLower(leak)) {
|
||
|
|
t.Fatalf("config sanity leaked %q in %s", leak, body)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if cfg["openai_key_set"] != true || cfg["stripe_secret_set"] != true || cfg["stripe_webhook_secret_set"] != true {
|
||
|
|
t.Fatalf("expected presence flags true, got openai=%v stripe=%v webhook=%v",
|
||
|
|
cfg["openai_key_set"], cfg["stripe_secret_set"], cfg["stripe_webhook_secret_set"])
|
||
|
|
}
|
||
|
|
if cfg["smtp_enabled"] != true || cfg["email_dry_run"] != true || cfg["smtp_host_set"] != true {
|
||
|
|
t.Fatalf("expected mail presence flags true, got enabled=%v dry_run=%v host_set=%v",
|
||
|
|
cfg["smtp_enabled"], cfg["email_dry_run"], cfg["smtp_host_set"])
|
||
|
|
}
|
||
|
|
if cfg["stripe_mock_rejected_in_prod"] != true {
|
||
|
|
t.Fatalf("expected stripe_mock_rejected_in_prod=true, got %v", cfg["stripe_mock_rejected_in_prod"])
|
||
|
|
}
|
||
|
|
if _, ok := cfg["database_url"]; ok {
|
||
|
|
t.Fatal("database_url must not appear in config sanity")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDiagMailConfigStatusPresenceOnly(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
t.Run("ready dry-run with host", func(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
s := &Server{
|
||
|
|
Config: config.Config{
|
||
|
|
SMTPEnabled: true,
|
||
|
|
SMTPHost: "smtp.secret-host.example",
|
||
|
|
SMTPPassword: "smtp-password-value",
|
||
|
|
EmailDryRun: true,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
check := s.diagMail()
|
||
|
|
if check["status"] != "ok" || check["enabled"] != true || check["dry_run"] != true || check["host_set"] != true {
|
||
|
|
t.Fatalf("check=%v", check)
|
||
|
|
}
|
||
|
|
raw, err := json.Marshal(check)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
body := strings.ToLower(string(raw))
|
||
|
|
for _, leak := range []string{"smtp.secret-host", "smtp-password", "smtp_password"} {
|
||
|
|
if strings.Contains(body, leak) {
|
||
|
|
t.Fatalf("mail check leaked %q in %s", leak, body)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("enabled without host warns", func(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
s := &Server{Config: config.Config{SMTPEnabled: true, EmailDryRun: false}}
|
||
|
|
check := s.diagMail()
|
||
|
|
if check["status"] != "warn" || check["host_set"] != false || check["dry_run"] != false {
|
||
|
|
t.Fatalf("check=%v", check)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("disabled noop", func(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
s := &Server{Config: config.Config{EmailDryRun: true}}
|
||
|
|
check := s.diagMail()
|
||
|
|
if check["status"] != "ok" || check["enabled"] != false || check["dry_run"] != true {
|
||
|
|
t.Fatalf("check=%v", check)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDiagCutoverReadinessNilPool(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
s := &Server{Config: config.Config{AppEnv: "test"}}
|
||
|
|
cutover := s.diagCutoverReadiness(context.Background())
|
||
|
|
if cutover["status"] != "warn" {
|
||
|
|
t.Fatalf("status=%v want warn", cutover["status"])
|
||
|
|
}
|
||
|
|
goose, _ := cutover["goose"].(map[string]any)
|
||
|
|
if goose["status"] != "skip" {
|
||
|
|
t.Fatalf("goose.status=%v want skip", goose["status"])
|
||
|
|
}
|
||
|
|
if _, ok := goose["version_max"]; ok {
|
||
|
|
t.Fatal("nil pool must not invent goose version_max")
|
||
|
|
}
|
||
|
|
worker, _ := cutover["worker"].(map[string]any)
|
||
|
|
if worker["status"] != "unavailable" {
|
||
|
|
t.Fatalf("worker.status=%v want unavailable", worker["status"])
|
||
|
|
}
|
||
|
|
if _, ok := worker["last_seen_age_s"]; ok {
|
||
|
|
t.Fatal("nil pool must omit last_seen_age_s")
|
||
|
|
}
|
||
|
|
if _, ok := cutover["companies_without_plan"]; ok {
|
||
|
|
t.Fatal("nil pool must omit companies_without_plan")
|
||
|
|
}
|
||
|
|
raw, err := json.Marshal(cutover)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
body := strings.ToLower(string(raw))
|
||
|
|
for _, leak := range []string{"postgres://", "sk_live", "password=", "smtp_password", "bearer "} {
|
||
|
|
if strings.Contains(body, leak) {
|
||
|
|
t.Fatalf("cutover leaked %q in %s", leak, body)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDiagMigrationInventoryNilPool(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
s := &Server{Config: config.Config{AppEnv: "test"}}
|
||
|
|
inv := s.diagMigrationInventory(context.Background())
|
||
|
|
if inv["status"] != "skip" {
|
||
|
|
t.Fatalf("status=%v want skip", inv["status"])
|
||
|
|
}
|
||
|
|
if inv["jobs_domain_ran"] != false {
|
||
|
|
t.Fatalf("jobs_domain_ran=%v want false", inv["jobs_domain_ran"])
|
||
|
|
}
|
||
|
|
for _, key := range []string{
|
||
|
|
"files_total", "files_metadata_only",
|
||
|
|
"processing_jobs_total", "processing_jobs_migrated", "tasks_total",
|
||
|
|
} {
|
||
|
|
n, ok := inv[key].(int64)
|
||
|
|
if !ok || n != 0 {
|
||
|
|
t.Fatalf("%s=%v want int64(0)", key, inv[key])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
notes, ok := inv["notes"].([]string)
|
||
|
|
if !ok || len(notes) == 0 {
|
||
|
|
t.Fatalf("notes=%v want non-empty []string", inv["notes"])
|
||
|
|
}
|
||
|
|
raw, err := json.Marshal(inv)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
body := strings.ToLower(string(raw))
|
||
|
|
for _, leak := range []string{"postgres://", "sk_live", "password=", "/var/", "c:\\"} {
|
||
|
|
if strings.Contains(body, leak) {
|
||
|
|
t.Fatalf("migration inventory leaked %q in %s", leak, body)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandleAdminDiagnosticsIncludesMigrationInventory(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
dir := t.TempDir()
|
||
|
|
s := &Server{Config: config.Config{UploadDir: dir, AppEnv: "test"}}
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/api/admin/diagnostics", nil)
|
||
|
|
rec := httptest.NewRecorder()
|
||
|
|
s.handleAdminDiagnostics(rec, req)
|
||
|
|
if rec.Code != http.StatusOK {
|
||
|
|
t.Fatalf("status=%d want 200 body=%s", rec.Code, rec.Body.String())
|
||
|
|
}
|
||
|
|
var body map[string]any
|
||
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||
|
|
t.Fatalf("json: %v", err)
|
||
|
|
}
|
||
|
|
inv, ok := body["migration_inventory"].(map[string]any)
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("migration_inventory missing: %#v", body["migration_inventory"])
|
||
|
|
}
|
||
|
|
if inv["status"] != "skip" {
|
||
|
|
t.Fatalf("migration_inventory.status=%v want skip (nil pool)", inv["status"])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDiagStripeReadiness(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
|
||
|
|
t.Run("prod mock fails", func(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
check, ok := diagStripeReadiness(true, billing.StripeConfig{
|
||
|
|
SecretKey: "sk_live_x", WebhookSecret: "whsec_x", ForceMock: true,
|
||
|
|
})
|
||
|
|
if ok || check["status"] != "fail" || check["mock_rejected_in_prod"] != false {
|
||
|
|
t.Fatalf("check=%v ok=%v", check, ok)
|
||
|
|
}
|
||
|
|
raw, _ := json.Marshal(check)
|
||
|
|
if strings.Contains(strings.ToLower(string(raw)), "sk_live") || strings.Contains(string(raw), "whsec_") {
|
||
|
|
t.Fatalf("leaked secret material: %s", raw)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("prod keys present", func(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
check, ok := diagStripeReadiness(true, billing.StripeConfig{
|
||
|
|
SecretKey: "sk_live_x", WebhookSecret: "whsec_x",
|
||
|
|
})
|
||
|
|
if !ok || check["status"] != "ok" || check["secret_key_set"] != true || check["webhook_secret_set"] != true {
|
||
|
|
t.Fatalf("check=%v ok=%v", check, ok)
|
||
|
|
}
|
||
|
|
if check["mock_rejected_in_prod"] != true {
|
||
|
|
t.Fatalf("mock_rejected_in_prod=%v", check["mock_rejected_in_prod"])
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("prod missing webhook warns", func(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
check, ok := diagStripeReadiness(true, billing.StripeConfig{SecretKey: "sk_live_x"})
|
||
|
|
if ok || check["status"] != "warn" || check["webhook_secret_set"] != false {
|
||
|
|
t.Fatalf("check=%v ok=%v", check, ok)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
t.Run("dev mock ok", func(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
check, ok := diagStripeReadiness(false, billing.StripeConfig{ForceMock: true})
|
||
|
|
if !ok || check["status"] != "ok" || check["mock"] != true {
|
||
|
|
t.Fatalf("check=%v ok=%v", check, ok)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDiagJobErrorUsesTruncateError(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
// Contract lock: job error strings must go through TruncateError before JSON.
|
||
|
|
secretish := "provider failed authorization: Bearer sk-abcdefghijklmnopqrstuvwxyz012345"
|
||
|
|
redacted := processing.TruncateError(errors.New(secretish))
|
||
|
|
if strings.Contains(strings.ToLower(redacted), "sk-abcdef") || strings.Contains(strings.ToLower(redacted), "bearer sk-") {
|
||
|
|
t.Fatalf("TruncateError did not redact: %q", redacted)
|
||
|
|
}
|
||
|
|
if redacted == "" {
|
||
|
|
t.Fatal("expected non-empty redacted message")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestHandleAdminDiagnosticsStorageWritable(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
dir := t.TempDir()
|
||
|
|
s := &Server{Config: config.Config{UploadDir: dir}}
|
||
|
|
check, ok := s.diagStorage()
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("expected writable storage check=%v", check)
|
||
|
|
}
|
||
|
|
if check["status"] != "ok" {
|
||
|
|
t.Fatalf("status=%v", check["status"])
|
||
|
|
}
|
||
|
|
// Absolute path must not appear in detail.
|
||
|
|
if detail, _ := check["detail"].(string); filepath.IsAbs(detail) {
|
||
|
|
t.Fatalf("detail must not be absolute path: %q", detail)
|
||
|
|
}
|
||
|
|
entries, err := os.ReadDir(dir)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
for _, e := range entries {
|
||
|
|
if e.Name() == ".diag_write_probe" {
|
||
|
|
t.Fatal("probe file should be removed")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRouterAdminDiagnosticsMounted(t *testing.T) {
|
||
|
|
t.Parallel()
|
||
|
|
sm := scs.New()
|
||
|
|
sm.Cookie.Name = "descrybe_session"
|
||
|
|
uid := uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
||
|
|
s := &Server{
|
||
|
|
Config: config.Config{
|
||
|
|
CSRFCookieName: "descrybe_csrf",
|
||
|
|
WebOrigin: "http://localhost:5173",
|
||
|
|
UploadDir: t.TempDir(),
|
||
|
|
},
|
||
|
|
Sessions: sm,
|
||
|
|
Auth: &auth.Service{},
|
||
|
|
testPlatformAdmin: func(_ context.Context, got uuid.UUID) (bool, error) {
|
||
|
|
return got == uid, nil
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
var token string
|
||
|
|
seed := LoadSession(sm)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
sm.Put(r.Context(), auth.SessionUserIDKey, uid.String())
|
||
|
|
w.WriteHeader(http.StatusNoContent)
|
||
|
|
}))
|
||
|
|
seedRec := httptest.NewRecorder()
|
||
|
|
seed.ServeHTTP(seedRec, httptest.NewRequest(http.MethodGet, "/seed", nil))
|
||
|
|
for _, c := range seedRec.Result().Cookies() {
|
||
|
|
if c.Name == sm.Cookie.Name {
|
||
|
|
token = c.Value
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if token == "" {
|
||
|
|
t.Fatal("expected session cookie from seed request")
|
||
|
|
}
|
||
|
|
|
||
|
|
h := s.Router()
|
||
|
|
|
||
|
|
unauth := httptest.NewRecorder()
|
||
|
|
h.ServeHTTP(unauth, httptest.NewRequest(http.MethodGet, "/api/admin/diagnostics", nil))
|
||
|
|
if unauth.Code != http.StatusUnauthorized {
|
||
|
|
t.Fatalf("unauth status=%d want 401 body=%s", unauth.Code, unauth.Body.String())
|
||
|
|
}
|
||
|
|
|
||
|
|
mounted := httptest.NewRecorder()
|
||
|
|
req := httptest.NewRequest(http.MethodGet, "/api/admin/diagnostics", nil)
|
||
|
|
req.AddCookie(&http.Cookie{Name: sm.Cookie.Name, Value: token})
|
||
|
|
h.ServeHTTP(mounted, req)
|
||
|
|
if mounted.Code == http.StatusNotFound {
|
||
|
|
t.Fatalf("diagnostics not mounted: status=404 body=%s", mounted.Body.String())
|
||
|
|
}
|
||
|
|
if mounted.Code != http.StatusOK {
|
||
|
|
t.Fatalf("mounted status=%d want 200 body=%s", mounted.Code, mounted.Body.String())
|
||
|
|
}
|
||
|
|
}
|