package api import ( "bytes" "io" "mime/multipart" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "chfs-ng/internal/auth" "chfs-ng/internal/storage" ) func permissionTestServer(t *testing.T, mask auth.PermMask) (*Server, string) { t.Helper() root := t.TempDir() if err := os.MkdirAll(filepath.Join(root, "dir"), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "file.txt"), []byte("hello"), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(root, "dir", "nested.txt"), []byte("nested"), 0o644); err != nil { t.Fatal(err) } s := &Server{ Store: storage.Local{Root: root}, Auth: &auth.Manager{ Accounts: map[string]*auth.Account{}, Guest: &auth.Account{Mask: mask}, }, RemoveMode: 3, ImagePreview: true, } return s, root } func doRequest(h http.Handler, method, target string, body io.Reader, contentType string) *httptest.ResponseRecorder { req := httptest.NewRequest(method, target, body) if contentType != "" { req.Header.Set("Content-Type", contentType) } rr := httptest.NewRecorder() h.ServeHTTP(rr, req) return rr } func multipartField(t *testing.T, field, value string) (*bytes.Buffer, string) { t.Helper() var b bytes.Buffer mw := multipart.NewWriter(&b) if err := mw.WriteField(field, value); err != nil { t.Fatal(err) } if err := mw.Close(); err != nil { t.Fatal(err) } return &b, mw.FormDataContentType() } func multipartUpload(t *testing.T, name, data string) (*bytes.Buffer, string) { t.Helper() var b bytes.Buffer mw := multipart.NewWriter(&b) fw, err := mw.CreateFormFile("file", name) if err != nil { t.Fatal(err) } if _, err := io.WriteString(fw, data); err != nil { t.Fatal(err) } if err := mw.WriteField("folder", ""); err != nil { t.Fatal(err) } if err := mw.Close(); err != nil { t.Fatal(err) } return &b, mw.FormDataContentType() } func TestHTTPPermissionMatrix(t *testing.T) { cases := []struct { name string mask auth.PermMask read int write int del int }{ {"none", auth.None, http.StatusForbidden, http.StatusForbidden, http.StatusForbidden}, {"read", auth.Read, http.StatusOK, http.StatusForbidden, http.StatusForbidden}, {"read-write", auth.Read | auth.Write, http.StatusOK, http.StatusCreated, http.StatusForbidden}, {"read-write-delete", auth.Read | auth.Write | auth.Delete, http.StatusOK, http.StatusCreated, http.StatusNoContent}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { s, root := permissionTestServer(t, tc.mask) h := s.Handler() files := doRequest(h, http.MethodGet, "/chfs/files?filepath=", nil, "") if files.Code != http.StatusOK { t.Fatalf("files: got %d", files.Code) } body, ct := multipartUpload(t, "uploaded.txt", "upload") upload := doRequest(h, http.MethodPost, "/chfs/upload", body, ct) if upload.Code != tc.write { t.Errorf("upload: got %d want %d", upload.Code, tc.write) } _, _ = os.Stat(filepath.Join(root, "uploaded.txt")) body, ct = multipartField(t, "filepath", "newdir") newdir := doRequest(h, http.MethodPost, "/chfs/newdir", body, ct) if newdir.Code != tc.write { t.Errorf("newdir: got %d want %d", newdir.Code, tc.write) } body, ct = multipartField(t, "old", "file.txt") body2, _ := multipartField(t, "new", "renamed.txt") var renameBody bytes.Buffer renameBody.Write(body.Bytes()) renameBody.Write(body2.Bytes()) // Rebuild the combined multipart request because each helper owns its boundary. var rb bytes.Buffer mw := multipart.NewWriter(&rb) _ = mw.WriteField("old", "file.txt") _ = mw.WriteField("new", "renamed.txt") _ = mw.Close() _ = body _ = ct rename := doRequest(h, http.MethodPost, "/chfs/rename", &rb, mw.FormDataContentType()) if rename.Code != tc.write { t.Errorf("rename: got %d want %d", rename.Code, tc.write) } deleteBody, deleteCT := multipartField(t, "filepath", "dir/nested.txt") del := doRequest(h, http.MethodDelete, "/chfs/rmfiles", deleteBody, deleteCT) if del.Code != tc.del { t.Errorf("delete: got %d want %d", del.Code, tc.del) } zip := doRequest(h, http.MethodGet, "/chfs/downloaddir/dir", nil, "") if zip.Code != tc.read { t.Errorf("zip: got %d want %d", zip.Code, tc.read) } }) } }