package httpapi import ( "bytes" "encoding/json" "mime/multipart" "net/http" "net/http/httptest" "strings" "testing" "time" ) func TestCreateScratchTTLQueryOmitUsesDefault(t *testing.T) { t.Parallel() handler := newTestHandler(t, testServerOptions{ maxUploadBytes: 1024, defaultTTL: 15 * time.Minute, rateLimitEnable: false, }) before := time.Now().UTC() req := httptest.NewRequest(http.MethodPost, "/api/scratch", bytes.NewBufferString("omit-ttl")) req.Header.Set("Content-Type", "text/plain; charset=utf-8") req.RemoteAddr = "127.0.0.1:8101" rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("status = %d, want %d body=%q", rec.Code, http.StatusCreated, rec.Body.String()) } var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode payload: %v", err) } expiresAt, err := time.Parse(time.RFC3339Nano, payload["expires_at"].(string)) if err != nil { // JSON may marshal as RFC3339 expiresAt, err = time.Parse(time.RFC3339, payload["expires_at"].(string)) if err != nil { t.Fatalf("parse expires_at %v (%T): %v", payload["expires_at"], payload["expires_at"], err) } } delta := expiresAt.Sub(before) if delta < 14*time.Minute || delta > 16*time.Minute { t.Fatalf("expires delta = %s, want ~15m", delta) } } func TestCreateScratchTTLQueryMinMaxAndInvalid(t *testing.T) { t.Parallel() handler := newTestHandler(t, testServerOptions{ maxUploadBytes: 1024, defaultTTL: 15 * time.Minute, rateLimitEnable: false, }) cases := []struct { name string ttl string wantStatus int wantDelta time.Duration deltaSlack time.Duration }{ {name: "min", ttl: "1m", wantStatus: http.StatusCreated, wantDelta: time.Minute, deltaSlack: 15 * time.Second}, {name: "max", ttl: "24h", wantStatus: http.StatusCreated, wantDelta: 24 * time.Hour, deltaSlack: time.Minute}, {name: "custom", ttl: "2h", wantStatus: http.StatusCreated, wantDelta: 2 * time.Hour, deltaSlack: 30 * time.Second}, {name: "below-min", ttl: "30s", wantStatus: http.StatusBadRequest}, {name: "above-max", ttl: "25h", wantStatus: http.StatusBadRequest}, {name: "invalid", ttl: "not-a-duration", wantStatus: http.StatusBadRequest}, {name: "zero", ttl: "0s", wantStatus: http.StatusBadRequest}, {name: "negative", ttl: "-5m", wantStatus: http.StatusBadRequest}, } for _, tc := range cases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() before := time.Now().UTC() req := httptest.NewRequest(http.MethodPost, "/api/scratch?ttl="+tc.ttl, bytes.NewBufferString("body-"+tc.name)) req.Header.Set("Content-Type", "text/plain; charset=utf-8") req.RemoteAddr = "127.0.0.1:8200" rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != tc.wantStatus { t.Fatalf("status = %d, want %d body=%q", rec.Code, tc.wantStatus, rec.Body.String()) } if tc.wantStatus != http.StatusCreated { if !strings.Contains(rec.Body.String(), "ttl") { t.Fatalf("expected ttl error message, got %q", rec.Body.String()) } return } var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode payload: %v", err) } expiresRaw, _ := payload["expires_at"].(string) expiresAt, err := time.Parse(time.RFC3339Nano, expiresRaw) if err != nil { expiresAt, err = time.Parse(time.RFC3339, expiresRaw) if err != nil { t.Fatalf("parse expires_at %q: %v", expiresRaw, err) } } delta := expiresAt.Sub(before) if delta < tc.wantDelta-tc.deltaSlack || delta > tc.wantDelta+tc.deltaSlack { t.Fatalf("expires delta = %s, want ~%s", delta, tc.wantDelta) } }) } } func TestCreateScratchTTLMultipartFormField(t *testing.T) { t.Parallel() handler := newTestHandler(t, testServerOptions{ maxUploadBytes: 1024, defaultTTL: 15 * time.Minute, rateLimitEnable: false, }) var body bytes.Buffer writer := multipart.NewWriter(&body) file, err := writer.CreateFormFile("file", "ttl.txt") if err != nil { t.Fatalf("CreateFormFile: %v", err) } if _, err := file.Write([]byte("multipart-ttl")); err != nil { t.Fatalf("write file: %v", err) } if err := writer.WriteField("ttl", "5m"); err != nil { t.Fatalf("WriteField ttl: %v", err) } if err := writer.Close(); err != nil { t.Fatalf("close writer: %v", err) } before := time.Now().UTC() req := httptest.NewRequest(http.MethodPost, "/api/scratch", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.RemoteAddr = "127.0.0.1:8301" rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Fatalf("status = %d, want %d body=%q", rec.Code, http.StatusCreated, rec.Body.String()) } var payload map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("decode payload: %v", err) } expiresRaw, _ := payload["expires_at"].(string) expiresAt, err := time.Parse(time.RFC3339Nano, expiresRaw) if err != nil { expiresAt, err = time.Parse(time.RFC3339, expiresRaw) if err != nil { t.Fatalf("parse expires_at %q: %v", expiresRaw, err) } } delta := expiresAt.Sub(before) if delta < 4*time.Minute || delta > 6*time.Minute { t.Fatalf("expires delta = %s, want ~5m", delta) } } func TestCreateScratchTTLMultipartInvalid(t *testing.T) { t.Parallel() handler := newTestHandler(t, testServerOptions{ maxUploadBytes: 1024, defaultTTL: 15 * time.Minute, rateLimitEnable: false, }) var body bytes.Buffer writer := multipart.NewWriter(&body) file, _ := writer.CreateFormFile("file", "bad-ttl.txt") _, _ = file.Write([]byte("x")) _ = writer.WriteField("ttl", "forever") _ = writer.Close() req := httptest.NewRequest(http.MethodPost, "/api/scratch", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.RemoteAddr = "127.0.0.1:8302" rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status = %d, want %d body=%q", rec.Code, http.StatusBadRequest, rec.Body.String()) } } func TestGetUIConfigExposesMaxTTL(t *testing.T) { t.Parallel() handler := newTestHandler(t, testServerOptions{ maxUploadBytes: 2048, defaultTTL: 15 * time.Minute, rateLimitEnable: false, }) // DefaultTTL string is empty in test helper; set via direct server call already covered. // Hit routed /api/config and ensure max_ttl is present. req := httptest.NewRequest(http.MethodGet, "/api/config", nil) req.RemoteAddr = "127.0.0.1:8401" rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) } body := rec.Body.String() if !strings.Contains(body, `"max_ttl"`) { t.Fatalf("expected max_ttl in body: %q", body) } }