package handlers_test import ( "bytes" "encoding/json" "mime/multipart" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func TestLibraryCreateListUpload(t *testing.T) { _, _, h, booksDir := setupAPI(t) tok := adminToken(t, h) root := filepath.Join(booksDir, "comics") // 服务端自动拼接 BooksDir/<清洗后的库名> w := do(h, "POST", "/api/libraries", tok, map[string]string{"name": "comics"}) if w.Code != 201 { t.Fatalf("create lib %d %s", w.Code, w.Body) } var lib map[string]any json.Unmarshal(w.Body.Bytes(), &lib) if lib["root_path"] != root { t.Fatalf("root_path want %q got %v", root, lib["root_path"]) } libID := itoa(lib["id"]) w = do(h, "GET", "/api/libraries", tok, nil) if !strings.Contains(w.Body.String(), `"comics"`) { t.Fatalf("list: %s", w.Body) } // 恶意库名必须清洗,root 仍在 booksDir 内 w = do(h, "POST", "/api/libraries", tok, map[string]string{"name": "../../etc/passwd"}) if w.Code != 201 || !strings.Contains(w.Body.String(), filepath.Join(booksDir, "passwd")) { t.Fatalf("traversal name want sanitized 201 got %d %s", w.Code, w.Body) } // "." / ".." 清洗后非法 → 400 w = do(h, "POST", "/api/libraries", tok, map[string]string{"name": ".."}) if w.Code != 400 { t.Fatalf("'..' want 400 got %d", w.Code) } // 重名(同 root)→ 409 w = do(h, "POST", "/api/libraries", tok, map[string]string{"name": "comics"}) if w.Code != 409 { t.Fatalf("dup want 409 got %d", w.Code) } // 上传:白名单 + 防穿越 + 原子落盘 body, mw := uploadBody("my 01.cbz", []byte("zipbytes")) req := httptest.NewRequest("POST", "/api/libraries/"+libID+"/upload", body) req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+tok) ww := httptest.NewRecorder() h.ServeHTTP(ww, req) if ww.Code != 202 { t.Fatalf("upload %d %s", ww.Code, ww.Body) } if _, err := os.Stat(filepath.Join(root, "my 01.cbz")); err != nil { t.Fatal("uploaded file missing:", err) } body, mw = uploadBody("../../evil.cbz", []byte("x")) req = httptest.NewRequest("POST", "/api/libraries/"+libID+"/upload", body) req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+tok) ww = httptest.NewRecorder() h.ServeHTTP(ww, req) if ww.Code != 202 { // 名字被清洗成 evil.cbz,落在 root 内 t.Fatalf("sanitize upload %d", ww.Code) } if _, err := os.Stat(filepath.Join(root, "evil.cbz")); err != nil { t.Fatal("evil upload not sanitized") } body, mw = uploadBody("virus.exe", []byte("x")) req = httptest.NewRequest("POST", "/api/libraries/"+libID+"/upload", body) req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+tok) ww = httptest.NewRecorder() h.ServeHTTP(ww, req) if ww.Code != 400 { t.Fatalf("bad ext want 400 got %d", ww.Code) } } func TestUploadSizeAndFilename(t *testing.T) { _, _, h, booksDir := setupAPI(t) tok := adminToken(t, h) w := do(h, "POST", "/api/libraries", tok, map[string]string{"name": "s2"}) if w.Code != 201 { t.Fatalf("create lib %d %s", w.Code, w.Body) } var lib map[string]any json.Unmarshal(w.Body.Bytes(), &lib) libID := itoa(lib["id"]) // 全角冒号等非 ASCII 文件名正常落盘 body, mw := uploadBody("調教開關:第二季.cbz", []byte("zipbytes")) req := httptest.NewRequest("POST", "/api/libraries/"+libID+"/upload", body) req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+tok) ww := httptest.NewRecorder() h.ServeHTTP(ww, req) if ww.Code != 202 { t.Fatalf("cjk name upload %d %s", ww.Code, ww.Body) } if _, err := os.Stat(filepath.Join(booksDir, "s2", "調教開關:第二季.cbz")); err != nil { t.Fatal("cjk upload missing:", err) } // 超过 UploadMaxMB(测试=1MB)→ 413 too_large,而非误报 "file required" body, mw = uploadBody("big.cbz", bytes.Repeat([]byte("x"), 2<<20)) req = httptest.NewRequest("POST", "/api/libraries/"+libID+"/upload", body) req.Header.Set("Content-Type", mw.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+tok) ww = httptest.NewRecorder() h.ServeHTTP(ww, req) if ww.Code != 413 || !strings.Contains(ww.Body.String(), "too_large") { t.Fatalf("oversize want 413 too_large got %d %s", ww.Code, ww.Body) } } func uploadBody(filename string, content []byte) (*bytes.Buffer, *multipart.Writer) { buf := &bytes.Buffer{} mw := multipart.NewWriter(buf) fw, _ := mw.CreateFormFile("file", filename) fw.Write(content) mw.Close() return buf, mw }