package api import ( "bytes" "encoding/binary" "hash/crc32" "image" "image/color" "image/png" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "chfs-ng/internal/auth" "chfs-ng/internal/storage" ) func TestAuditExistEnforcesReadPermission(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "secret.txt"), []byte("x"), 0600); err != nil { t.Fatal(err) } s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.None}}} rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/chfs/exist?file=secret.txt", nil) s.exist(rr, req) if rr.Code != 403 { t.Fatalf("exist status=%d want 403", rr.Code) } } func TestAuditThumbnailRejectsOversizeBeforeDecode(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "big.bin"), make([]byte, 64), 0600); err != nil { t.Fatal(err) } s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Read}}, ImagePreview: true, ThumbnailMaxSize: 32} rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/chfs/thumbnail?file=big.bin", nil) s.thumbnail(rr, req) if rr.Code != 413 { t.Fatalf("thumbnail status=%d want 413", rr.Code) } } func TestAuditZipLimitsUseOriginalFileSizes(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "a.txt"), make([]byte, 100), 0600); err != nil { t.Fatal(err) } s := &Server{Store: storage.Local{Root: root}, ZipMaxSize: 99, ZipMaxFiles: 10} a := &auth.Account{Mask: auth.Read} if err := s.checkZipLimits(a, []string{""}); err == nil { t.Fatal("expected zip size limit") } } func TestAuditSearchLimit(t *testing.T) { root := t.TempDir() for _, name := range []string{"a.txt", "b.txt", "c.txt"} { if err := os.WriteFile(filepath.Join(root, name), []byte(name), 0600); err != nil { t.Fatal(err) } } s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Read}}, SearchMaxResults: 2} rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/chfs/search?str=.txt", nil) s.search(rr, req) if rr.Code != 200 { t.Fatalf("search status=%d", rr.Code) } if len(rr.Body.Bytes()) == 0 || !contains(rr.Body.String(), `"truncated":true`) { t.Fatalf("search did not report truncation: %s", rr.Body.String()) } } func contains(s, sub string) bool { return len(s) >= len(sub) && (s == sub || func() bool { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return true } } return false }()) } func TestAuditThumbnailPixelLimit(t *testing.T) { root := t.TempDir() img := image.NewRGBA(image.Rect(0, 0, 20, 20)) img.Set(0, 0, color.White) f, err := os.Create(filepath.Join(root, "large.png")) if err != nil { t.Fatal(err) } if err := png.Encode(f, img); err != nil { f.Close() t.Fatal(err) } f.Close() s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Read}}, ImagePreview: true, ThumbnailMaxPixels: 100} rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/chfs/thumbnail?file=large.png", nil) s.thumbnail(rr, req) if rr.Code != 415 { t.Fatalf("thumbnail pixel-limit status=%d want 415", rr.Code) } } func TestAuditThumbnailRejectsDeclaredHugeDimensionsBeforeDecode(t *testing.T) { root := t.TempDir() f, err := os.Create(filepath.Join(root, "bomb.png")) if err != nil { t.Fatal(err) } img := image.NewRGBA(image.Rect(0, 0, 1, 1)) if err := png.Encode(f, img); err != nil { f.Close() t.Fatal(err) } if err := f.Close(); err != nil { t.Fatal(err) } data, err := os.ReadFile(filepath.Join(root, "bomb.png")) if err != nil { t.Fatal(err) } // PNG IHDR starts at byte 16. Replace the 1x1 declaration with a // deliberately huge image while keeping the file otherwise tiny/valid. binary.BigEndian.PutUint32(data[16:20], 100000) binary.BigEndian.PutUint32(data[20:24], 100000) crc := crc32.ChecksumIEEE(data[12:29]) binary.BigEndian.PutUint32(data[29:33], crc) if err := os.WriteFile(filepath.Join(root, "bomb.png"), data, 0600); err != nil { t.Fatal(err) } s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Read}}, ImagePreview: true, ThumbnailMaxPixels: 40_000_000} rr := httptest.NewRecorder() req := httptest.NewRequest("GET", "/chfs/thumbnail?file=bomb.png", nil) s.thumbnail(rr, req) if rr.Code != 415 { t.Fatalf("thumbnail bomb status=%d want 415", rr.Code) } } func TestAuditUploadMaxBytesReaderBeforeMultipartSpool(t *testing.T) { for _, tc := range []struct { name string handler func(*Server, http.ResponseWriter, *http.Request) }{ {"upload", (*Server).upload}, {"upload2", (*Server).upload2}, } { t.Run(tc.name, func(t *testing.T) { root := t.TempDir() var body bytes.Buffer mw := multipart.NewWriter(&body) part, err := mw.CreateFormFile("file", "oversize.bin") if err != nil { t.Fatal(err) } if _, err := part.Write(make([]byte, 1024)); err != nil { t.Fatal(err) } if err := mw.Close(); err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodPost, "/chfs/"+tc.name+"?folder=", &body) req.Header.Set("Content-Type", mw.FormDataContentType()) s := &Server{Store: storage.Local{Root: root}, Auth: &auth.Manager{Guest: &auth.Account{Mask: auth.Write}}, UploadMaxTotalSize: 128} rr := httptest.NewRecorder() tc.handler(s, rr, req) if rr.Code != http.StatusRequestEntityTooLarge { t.Fatalf("status=%d want 413", rr.Code) } }) } }