ad4355df17
CI / Go Tests (push) Successful in 15s
CI / Build (push) Successful in 9s
Format / gofmt (push) Successful in 7s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 15s
Release Artifacts / Build and release Docker image (push) Successful in 28s
188 lines
6.0 KiB
Go
188 lines
6.0 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/netip"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"scratchbox/internal/config"
|
|
"scratchbox/internal/storage"
|
|
)
|
|
|
|
func TestMultipartFileCount(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/scratch", nil)
|
|
if got := multipartFileCount(req); got != 0 {
|
|
t.Fatalf("multipartFileCount() = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestWriteCreateErrorBranches(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := &Server{}
|
|
rec1 := httptest.NewRecorder()
|
|
s.writeCreateError(rec1, &http.MaxBytesError{Limit: 5})
|
|
if rec1.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("max bytes error status = %d, want %d", rec1.Code, http.StatusRequestEntityTooLarge)
|
|
}
|
|
|
|
rec2 := httptest.NewRecorder()
|
|
s.writeCreateError(rec2, errors.New("request body too large"))
|
|
if rec2.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("body too large string status = %d, want %d", rec2.Code, http.StatusRequestEntityTooLarge)
|
|
}
|
|
|
|
rec3 := httptest.NewRecorder()
|
|
s.writeCreateError(rec3, errors.New("other parse failure"))
|
|
if rec3.Code != http.StatusBadRequest {
|
|
t.Fatalf("generic error status = %d, want %d", rec3.Code, http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func TestNewServerInitializes(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
store, err := storage.NewFilesystemStore(filepath.Join(tmp, "data"), testStorageKey)
|
|
if err != nil {
|
|
t.Fatalf("NewFilesystemStore() error = %v", err)
|
|
}
|
|
cfg := config.Config{
|
|
Limits: config.LimitsConfig{MaxUploadSizeBytes: 1024},
|
|
Security: config.SecurityConfig{
|
|
RateLimitUI: config.RateLimitConfig{Enabled: false, RequestsPerMinute: 30, Burst: 10},
|
|
RateLimitAPIRead: config.RateLimitConfig{Enabled: false, RequestsPerMinute: 30, Burst: 10},
|
|
RateLimitAPIWrite: config.RateLimitConfig{Enabled: false, RequestsPerMinute: 30, Burst: 10},
|
|
HSTSEnabled: true,
|
|
},
|
|
}
|
|
|
|
srv, err := NewServer(cfg, store, log.New(io.Discard, "", 0), log.New(io.Discard, "", 0))
|
|
if err != nil {
|
|
t.Fatalf("NewServer() error = %v", err)
|
|
}
|
|
if srv == nil {
|
|
t.Fatalf("expected initialized server")
|
|
}
|
|
}
|
|
|
|
func TestGetUIConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := &Server{
|
|
cfg: config.Config{
|
|
Limits: config.LimitsConfig{
|
|
MaxUploadSizeBytes: 2048,
|
|
},
|
|
},
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/config", nil)
|
|
s.getUIConfig(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
if got := rec.Body.String(); got == "" || !containsAll(got, "max_upload_size_bytes", "2048", `"upload_allowed":true`) {
|
|
t.Fatalf("unexpected body: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestGetUIConfigUploadDenied(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := &Server{
|
|
cfg: config.Config{
|
|
Limits: config.LimitsConfig{
|
|
MaxUploadSizeBytes: 2048,
|
|
},
|
|
Security: config.SecurityConfig{
|
|
AllowedPrefixes: []netip.Prefix{
|
|
netip.MustParsePrefix("10.0.0.0/8"),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/config", nil)
|
|
s.getUIConfig(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK)
|
|
}
|
|
if got := rec.Body.String(); got == "" || !containsAll(got, `"upload_allowed":false`) {
|
|
t.Fatalf("unexpected body: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestWriteCreateErrorBranchesWithConfig(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
store, err := storage.NewFilesystemStore(filepath.Join(tmp, "data"), testStorageKey)
|
|
if err != nil {
|
|
t.Fatalf("NewFilesystemStore() error = %v", err)
|
|
}
|
|
cfg := config.Config{
|
|
Limits: config.LimitsConfig{MaxUploadSizeBytes: 1024},
|
|
Security: config.SecurityConfig{
|
|
RateLimitUI: config.RateLimitConfig{Enabled: false, RequestsPerMinute: 30, Burst: 10},
|
|
RateLimitAPIRead: config.RateLimitConfig{Enabled: false, RequestsPerMinute: 30, Burst: 10},
|
|
RateLimitAPIWrite: config.RateLimitConfig{Enabled: false, RequestsPerMinute: 30, Burst: 10},
|
|
HSTSEnabled: true,
|
|
},
|
|
}
|
|
if _, err := NewServer(cfg, store, log.New(io.Discard, "", 0), log.New(io.Discard, "", 0)); err != nil {
|
|
t.Fatalf("NewServer() error = %v", err)
|
|
}
|
|
}
|
|
|
|
func containsAll(haystack string, needles ...string) bool {
|
|
for _, needle := range needles {
|
|
if !strings.Contains(haystack, needle) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// TestInjectMetaEscapesSpecialChars exercises the server-side meta injection
|
|
// (used for /s/{id} and 410 pages) with filenames/titles containing HTML special
|
|
// chars. This provides basic coverage/golden-like check for the replace+escape
|
|
// logic against the exact literals in the committed static shell (addresses
|
|
// fragility note without changing to full template).
|
|
func TestInjectMetaEscapesSpecialChars(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Minimal shell containing exactly the replace targets used by injectMeta.
|
|
// (Real index.html from Vite is larger but uses these literal strings.)
|
|
shell := []byte(`<!doctype html>
|
|
<html><head>
|
|
<title>Scratchbox</title>
|
|
<meta property="og:title" content="Scratchbox" />
|
|
<meta name="twitter:title" content="Scratchbox" />
|
|
<meta name="description" content="Minimal temporary file sharing. Upload once, share the link, auto-expires." />
|
|
<meta property="og:description" content="Minimal temporary file sharing. Upload once, share the link, auto-expires." />
|
|
<meta name="twitter:description" content="Minimal temporary file sharing. Upload once, share the link, auto-expires." />
|
|
</head><body></body></html>`)
|
|
|
|
tricky := `report & "notes" <2026> 'foo'`
|
|
gotBytes := injectMeta(shell, tricky, tricky, tricky)
|
|
got := string(gotBytes)
|
|
|
|
// Must have escaped the specials.
|
|
if !strings.Contains(got, `&`) || !strings.Contains(got, `<`) || !strings.Contains(got, `>`) || !strings.Contains(got, `"`) {
|
|
t.Fatalf("injectMeta did not escape specials in output: %q", got)
|
|
}
|
|
// Title etc updated (escaped form present).
|
|
if !strings.Contains(got, `report & "notes" <2026>`) {
|
|
t.Fatalf("injectMeta title not updated or badly escaped: %q", got)
|
|
}
|
|
// Original static should be gone (count=1 replaces).
|
|
if strings.Contains(got, `<title>Scratchbox</title>`) {
|
|
t.Fatalf("static title not replaced")
|
|
}
|
|
}
|