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(` Scratchbox `) 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, `Scratchbox`) { t.Fatalf("static title not replaced") } }