Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 40 additions & 322 deletions drivers/alist_v3/driver.go

Large diffs are not rendered by default.

233 changes: 206 additions & 27 deletions drivers/alist_v3/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,27 @@ import (
"time"

"github.com/OpenListTeam/OpenList/v4/drivers/base"
openlistdriver "github.com/OpenListTeam/OpenList/v4/drivers/openlist"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/go-resty/resty/v2"
)

// TestListSetsChildPaths descends two levels the way op.Get does, feeding an
// object from one listing back into List as dir. Without a Path on that object
// the driver asks upstream for "", which a real server answers with its own
// root -- hence the fake upstream's fallback, and the endless self-similar tree.
func TestListSetsChildPaths(t *testing.T) {
type familyDriver interface {
List(context.Context, model.Obj, model.ListArgs) ([]model.Obj, error)
ArchiveDecompress(context.Context, model.Obj, model.Obj, model.ArchiveDecompressArgs) error
}

func useTestClient(t *testing.T) {
t.Helper()
prev := base.RestyClient
base.RestyClient = resty.New().SetTimeout(5 * time.Second)
t.Cleanup(func() { base.RestyClient = prev })
}

// This descends two levels the way op.Get does, feeding an object from one
// listing back into List. It is one behavioral contract for both products.
func TestFamilyListSetsChildPaths(t *testing.T) {
tree := map[string][]ObjResp{
"/": {{Name: "root-marker", IsDir: true}},
"/drive": {{Name: "concerts", IsDir: true}},
Expand All @@ -31,35 +42,203 @@ func TestListSetsChildPaths(t *testing.T) {
if !ok {
content = tree["/"]
}
w.Header().Set("Content-Type", "application/json") // resty only unmarshals JSON
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"code": 200, "message": "success",
"data": map[string]any{"content": content, "total": len(content)},
})
}))
t.Cleanup(srv.Close)
useTestClient(t)

// conf.Conf is nil outside a booted server, so base.InitClient() is unusable.
prev := base.RestyClient
base.RestyClient = resty.New().SetTimeout(5 * time.Second)
t.Cleanup(func() { base.RestyClient = prev })
drivers := map[string]familyDriver{
"alist-v3": &AListV3{Addition: Addition{
RootPath: driver.RootPath{RootFolderPath: "/drive"},
Address: srv.URL,
}},
"openlist": &openlistdriver.OpenList{Addition: openlistdriver.Addition{
RootPath: driver.RootPath{RootFolderPath: "/drive"},
Address: srv.URL,
}},
}
for name, d := range drivers {
t.Run(name, func(t *testing.T) {
dir := model.Obj(&model.Object{Path: "/drive", IsFolder: true})
for _, want := range []string{"/drive/concerts", "/drive/concerts/show.mkv"} {
objs, err := d.List(context.Background(), dir, model.ListArgs{})
if err != nil {
t.Fatalf("List(%q): %v", dir.GetPath(), err)
}
if len(objs) != 1 {
t.Fatalf("List(%q) returned %d objects, want 1", dir.GetPath(), len(objs))
}
if got := objs[0].GetPath(); got != want {
t.Fatalf("child of %q has path %q, want %q", dir.GetPath(), got, want)
}
dir = objs[0]
}
})
}
}

d := &AListV3{Addition: Addition{
RootPath: driver.RootPath{RootFolderPath: "/drive"},
Address: srv.URL,
}}
dir := model.Obj(&model.Object{Path: "/drive", IsFolder: true})
for _, want := range []string{"/drive/concerts", "/drive/concerts/show.mkv"} {
objs, err := d.List(context.Background(), dir, model.ListArgs{})
if err != nil {
t.Fatalf("List(%q): %v", dir.GetPath(), err)
}
if len(objs) != 1 {
t.Fatalf("List(%q) returned %d objects, want 1", dir.GetPath(), len(objs))
}
if got := objs[0].GetPath(); got != want {
t.Fatalf("child of %q has path %q, want %q", dir.GetPath(), got, want)
}
dir = objs[0]
func TestFamilyDialectRequests(t *testing.T) {
tests := []struct {
name string
newDriver func(string) familyDriver
wantRefresh bool
wantOverwrite bool
wantOverwriteKey bool
}{
{
name: "alist-v3 keeps legacy request shape",
newDriver: func(address string) familyDriver {
return &AListV3{Addition: Addition{
Address: address, ForwardArchiveReq: true,
}}
},
},
{
name: "openlist suppresses refresh unless enabled",
newDriver: func(address string) familyDriver {
return &openlistdriver.OpenList{Addition: openlistdriver.Addition{
Address: address, ForwardArchiveReq: true,
}}
},
wantOverwrite: true, wantOverwriteKey: true,
},
{
name: "openlist forwards enabled flags",
newDriver: func(address string) familyDriver {
return &openlistdriver.OpenList{Addition: openlistdriver.Addition{
Address: address, ForwardArchiveReq: true, PassRefreshFlagToUpsteam: true,
}}
},
wantRefresh: true, wantOverwrite: true, wantOverwriteKey: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var listBody, decompressBody map[string]json.RawMessage
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/fs/list":
_ = json.NewDecoder(r.Body).Decode(&listBody)
case "/api/fs/archive/decompress":
_ = json.NewDecoder(r.Body).Decode(&decompressBody)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"code":200,"message":"success","data":{"content":[]}}`))
}))
t.Cleanup(srv.Close)
useTestClient(t)

d := tt.newDriver(srv.URL)
dir := &model.Object{Path: "/dst", IsFolder: true}
if _, err := d.List(context.Background(), dir, model.ListArgs{Refresh: true}); err != nil {
t.Fatal(err)
}
if err := d.ArchiveDecompress(
context.Background(),
&model.Object{Path: "/src/a.zip"},
dir,
model.ArchiveDecompressArgs{Overwrite: true},
); err != nil {
t.Fatal(err)
}

var gotRefresh bool
if err := json.Unmarshal(listBody["refresh"], &gotRefresh); err != nil {
t.Fatal(err)
}
if gotRefresh != tt.wantRefresh {
t.Fatalf("refresh = %v, want %v", gotRefresh, tt.wantRefresh)
}
raw, hasOverwrite := decompressBody["overwrite"]
if hasOverwrite != tt.wantOverwriteKey {
t.Fatalf("overwrite presence = %v, want %v", hasOverwrite, tt.wantOverwriteKey)
}
if hasOverwrite {
var gotOverwrite bool
if err := json.Unmarshal(raw, &gotOverwrite); err != nil {
t.Fatal(err)
}
if gotOverwrite != tt.wantOverwrite {
t.Fatalf("overwrite = %v, want %v", gotOverwrite, tt.wantOverwrite)
}
}
})
}
}

func TestRoleDialectDecoding(t *testing.T) {
var legacy MeResp
if err := json.Unmarshal([]byte(`{"role":[2]}`), &legacy); err != nil {
t.Fatalf("AList V3 array role: %v", err)
}
if len(legacy.Role) != 1 || legacy.Role[0] != 2 {
t.Fatalf("AList V3 role = %v, want [2]", legacy.Role)
}
if err := json.Unmarshal([]byte(`{"role":2}`), &legacy); err != nil {
t.Fatalf("AList V3 scalar role: %v", err)
}

var current openlistdriver.MeResp
if err := json.Unmarshal([]byte(`{"role":2}`), &current); err != nil {
t.Fatalf("OpenList scalar role: %v", err)
}
if err := json.Unmarshal([]byte(`{"role":[2]}`), &current); err == nil {
t.Fatal("OpenList unexpectedly accepted an array role")
}
}

func TestFamilyLinkPreservesForwardedContext(t *testing.T) {
type linkDriver interface {
Link(context.Context, model.Obj, model.LinkArgs) (*model.Link, error)
ResolveLinkCacheMode(string) driver.LinkCacheMode
}
for _, tt := range []struct {
name string
new func(string) linkDriver
}{
{"alist-v3", func(address string) linkDriver {
return &AListV3{Addition: Addition{
Address: address, MetaPassword: "secret", PassIPToUpsteam: true, PassUAToUpsteam: true,
}}
}},
{"openlist", func(address string) linkDriver {
return &openlistdriver.OpenList{Addition: openlistdriver.Addition{
Address: address, MetaPassword: "secret", PassIPToUpsteam: true, PassUAToUpsteam: true,
}}
}},
} {
t.Run(tt.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/fs/get" || r.Method != http.MethodPost {
t.Errorf("request = %s %s", r.Method, r.URL.Path)
}
if r.Header.Get("User-Agent") != "client" || r.Header.Get("X-Forwarded-For") != "192.0.2.1" || r.Header.Get("X-Real-Ip") != "192.0.2.1" {
t.Errorf("forwarded headers = %v", r.Header)
}
var body FsGetReq
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Path != "/movie" || body.Password != "secret" {
t.Errorf("link request = %+v, err = %v", body, err)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"code":200,"message":"success","data":{"raw_url":"https://example.invalid/movie"}}`))
}))
t.Cleanup(srv.Close)
useTestClient(t)
d := tt.new(srv.URL)
link, err := d.Link(context.Background(), &model.Object{Path: "/movie"}, model.LinkArgs{
IP: "192.0.2.1", Header: http.Header{"User-Agent": []string{"client"}},
})
if err != nil || link == nil || link.URL != "https://example.invalid/movie" {
t.Fatalf("Link = %+v, err = %v", link, err)
}
if got, want := d.ResolveLinkCacheMode("/movie"), driver.LinkCacheIP|driver.LinkCacheUA; got != want {
t.Fatalf("cache mode = %v, want %v", got, want)
}
})
}
}
Loading