package baidu import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "os" "strconv" "sync" "testing" "time" "gitea.dddbg.com/youbin/bdrclone/internal/config" ) func TestRemotePathWithRoot(t *testing.T) { client := New(testConfig("token")) for input, want := range map[string]string{ "/": "/apps/bdrclone", "docs/a.txt": "/apps/bdrclone/docs/a.txt", "/../a": "/apps/bdrclone/a", } { if got := client.RemotePath(input); got != want { t.Errorf("RemotePath(%q) = %q, want %q", input, got, want) } } if got := client.APIPath("/apps/bdrclone/docs/a.txt"); got != "/docs/a.txt" { t.Fatalf("APIPath = %q", got) } } func TestExpiredTokenRefreshesAndPersists(t *testing.T) { var saved Token var listCalls int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/oauth/2.0/token": if r.URL.Query().Get("refresh_token") != "refresh-old" { t.Errorf("unexpected refresh token: %q", r.URL.Query().Get("refresh_token")) } fmt.Fprint(w, `{"access_token":"access-new","refresh_token":"refresh-new","expires_in":3600}`) case "/rest/2.0/xpan/file": listCalls++ if r.URL.Query().Get("access_token") != "access-new" { t.Errorf("unexpected access token: %q", r.URL.Query().Get("access_token")) } if got := r.Header.Get("User-Agent"); got != "pan.baidu.com" { t.Errorf("User-Agent = %q", got) } fmt.Fprint(w, `{"errno":0,"list":[]}`) default: http.NotFound(w, r) } })) defer server.Close() cfg := testConfig("expired") cfg.RefreshToken = "refresh-old" cfg.ExpiresAt = time.Now().Add(-time.Hour) client := New(cfg, WithEndpoints(server.URL, server.URL, server.URL), WithTokenSaver(func(token Token, _ time.Time) error { saved = token return nil })) if _, err := client.List(context.Background(), "/"); err != nil { t.Fatal(err) } if saved.AccessToken != "access-new" || saved.RefreshToken != "refresh-new" { t.Fatalf("saved token = %+v", saved) } if listCalls != 1 { t.Fatalf("list calls = %d", listCalls) } } func TestOpenUsesOfficialUserAgentAndRange(t *testing.T) { var server *httptest.Server metadataCalls := 0 server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/rest/2.0/xpan/multimedia": metadataCalls++ fmt.Fprintf(w, `{"errno":0,"list":[{"fs_id":42,"dlink":%q}]}`, server.URL+"/download?x=1") case "/download": if got := r.Header.Get("User-Agent"); got != "pan.baidu.com" { t.Errorf("User-Agent = %q", got) } if got := r.Header.Get("Range"); got != "bytes=2-5" { t.Errorf("Range = %q", got) } if got := r.URL.Query().Get("access_token"); got != "token" { t.Errorf("access_token = %q", got) } w.WriteHeader(http.StatusPartialContent) fmt.Fprint(w, "2345") default: http.NotFound(w, r) } })) defer server.Close() client := New(testConfig("token"), WithEndpoints(server.URL, server.URL, server.URL)) body, err := client.Open(context.Background(), File{FSID: 42, Path: "/a"}, 2, 4) if err != nil { t.Fatal(err) } defer body.Close() b, err := io.ReadAll(body) if err != nil { t.Fatal(err) } if string(b) != "2345" { t.Fatalf("body = %q", b) } body, err = client.Open(context.Background(), File{FSID: 42, Path: "/a"}, 2, 4) if err != nil { t.Fatal(err) } body.Close() if metadataCalls != 1 { t.Fatalf("download metadata calls = %d, want cached URL to be reused", metadataCalls) } } func TestUploadMultipartFlow(t *testing.T) { content := append(bytes.Repeat([]byte("a"), int(defaultPartSize)), []byte("tail")...) file, err := os.CreateTemp(t.TempDir(), "upload-*") if err != nil { t.Fatal(err) } if _, err := file.Write(content); err != nil { t.Fatal(err) } var mu sync.Mutex parts := map[int][]byte{} server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/rest/2.0/xpan/file": if err := r.ParseForm(); err != nil { t.Error(err) } switch r.URL.Query().Get("method") { case "precreate": if r.Form.Get("path") != "/apps/bdrclone/remote.bin" { t.Errorf("precreate path = %q", r.Form.Get("path")) } var blocks []string if err := json.Unmarshal([]byte(r.Form.Get("block_list")), &blocks); err != nil || len(blocks) != 2 { t.Errorf("block list = %q, err=%v", r.Form.Get("block_list"), err) } fmt.Fprint(w, `{"errno":0,"return_type":1,"uploadid":"upload-1","block_list":[0,1]}`) case "create": if r.Form.Get("uploadid") != "upload-1" { t.Errorf("create uploadid = %q", r.Form.Get("uploadid")) } fmt.Fprint(w, `{"errno":0,"fs_id":99,"path":"/apps/bdrclone/remote.bin","server_filename":"remote.bin","size":4194308}`) default: http.Error(w, "unexpected method", http.StatusBadRequest) } case "/rest/2.0/pcs/superfile2": part, _ := strconv.Atoi(r.URL.Query().Get("partseq")) if r.URL.Query().Get("uploadid") != "upload-1" { t.Errorf("part uploadid = %q", r.URL.Query().Get("uploadid")) } if err := r.ParseMultipartForm(defaultPartSize + 1024); err != nil { t.Error(err) return } partFile, _, err := r.FormFile("file") if err != nil { t.Error(err) return } b, err := io.ReadAll(partFile) partFile.Close() if err != nil { t.Error(err) } mu.Lock() parts[part] = b mu.Unlock() fmt.Fprint(w, `{"md5":"ok"}`) default: http.NotFound(w, r) } })) defer server.Close() cfg := testConfig("token") cfg.PartSize = defaultPartSize cfg.UploadParts = 2 client := New(cfg, WithEndpoints(server.URL, server.URL, server.URL)) entry, err := client.Upload(context.Background(), file, int64(len(content)), "/remote.bin", time.Unix(1_700_000_000, 0), nil) if err != nil { t.Fatal(err) } if entry.FSID != 99 { t.Fatalf("entry = %+v", entry) } mu.Lock() defer mu.Unlock() if !bytes.Equal(parts[0], content[:defaultPartSize]) || !bytes.Equal(parts[1], content[defaultPartSize:]) { t.Fatalf("uploaded parts do not match input: sizes %d, %d", len(parts[0]), len(parts[1])) } } func testConfig(token string) *config.Config { return &config.Config{ ClientID: "client", ClientSecret: "secret", AccessToken: token, Root: "/apps/bdrclone", UserAgent: "pan.baidu.com", UploadParts: 1, } }