ed95ce8295
Format / gofmt (push) Successful in 26s
Format / gofmt (pull_request) Successful in 26s
CI / Build (push) Successful in 33s
CI / Build (pull_request) Successful in 33s
CI / Go Tests (push) Successful in 35s
CI / Go Tests (pull_request) Successful in 35s
#4
218 lines
6.7 KiB
Go
218 lines
6.7 KiB
Go
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)
|
|
}
|
|
}
|