mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 00:50:27 +00:00
728 lines
23 KiB
Go
728 lines
23 KiB
Go
package download
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/GopeedLab/gopeed/pkg/base"
|
|
"github.com/GopeedLab/gopeed/pkg/util"
|
|
)
|
|
|
|
func newTestCategoryConfig() *base.DownloaderStoreConfig {
|
|
return &base.DownloaderStoreConfig{
|
|
DownloadDir: "./downloads",
|
|
AutoCategorize: util.BoolPtr(true),
|
|
Categories: []*base.DownloadCategory{
|
|
{Name: "Music", Path: filepath.Join("./downloads", "Music"), Extensions: []string{"mp3", "flac"}},
|
|
{Name: "Video", Path: filepath.Join("./downloads", "Video"), Extensions: []string{"mp4", "mkv"}},
|
|
{Name: "Program", Path: filepath.Join("./downloads", "Program"), Extensions: []string{"exe", "msi"}},
|
|
},
|
|
}
|
|
}
|
|
|
|
// singleFileResource is the resource shape of a one file task: no folder name
|
|
// and a single file entry.
|
|
func singleFileResource(name string) *base.Resource {
|
|
return &base.Resource{Files: []*base.FileInfo{{Name: name, Size: 1}}}
|
|
}
|
|
|
|
// folderResource is the resource shape of a multi file task: a folder name and
|
|
// one entry per file, even when only one of them is selected later.
|
|
func folderResource(name string, files ...string) *base.Resource {
|
|
res := &base.Resource{Name: name}
|
|
for _, file := range files {
|
|
res.Files = append(res.Files, &base.FileInfo{Name: file, Size: 1})
|
|
}
|
|
return res
|
|
}
|
|
|
|
func newUnitCategoryDownloader(cfg *base.DownloaderStoreConfig) *Downloader {
|
|
d := NewDownloader(nil)
|
|
d.cfg.DownloaderStoreConfig = cfg
|
|
return d
|
|
}
|
|
|
|
func TestDecideDownloadDir(t *testing.T) {
|
|
programDir := filepath.Join("./downloads", "Program")
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfg func(cfg *base.DownloaderStoreConfig)
|
|
opts *base.Options
|
|
res *base.Resource
|
|
want string
|
|
}{
|
|
{
|
|
name: "matching single file routes to the category",
|
|
opts: &base.Options{},
|
|
res: singleFileResource("setup.exe"),
|
|
want: programDir,
|
|
},
|
|
{
|
|
name: "an explicit base directory is routed too",
|
|
opts: &base.Options{Path: "./elsewhere"},
|
|
res: singleFileResource("setup.exe"),
|
|
want: programDir,
|
|
},
|
|
{
|
|
name: "skip flag keeps the configured download directory",
|
|
opts: &base.Options{Path: "./downloads", SkipAutoCategorize: true},
|
|
res: singleFileResource("setup.exe"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "skip flag keeps an explicit directory",
|
|
opts: &base.Options{Path: "./elsewhere", SkipAutoCategorize: true},
|
|
res: singleFileResource("setup.exe"),
|
|
want: "./elsewhere",
|
|
},
|
|
{
|
|
name: "switch off keeps the base directory",
|
|
cfg: func(cfg *base.DownloaderStoreConfig) { cfg.AutoCategorize = util.BoolPtr(false) },
|
|
opts: &base.Options{},
|
|
res: singleFileResource("setup.exe"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "no categories keeps the base directory",
|
|
cfg: func(cfg *base.DownloaderStoreConfig) { cfg.Categories = nil },
|
|
opts: &base.Options{},
|
|
res: singleFileResource("setup.exe"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "unknown extension keeps the base directory",
|
|
opts: &base.Options{},
|
|
res: singleFileResource("archive.zip"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "a name without an extension keeps the base directory",
|
|
opts: &base.Options{},
|
|
res: singleFileResource("README"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "a deleted category is skipped",
|
|
cfg: func(cfg *base.DownloaderStoreConfig) {
|
|
cfg.Categories = []*base.DownloadCategory{
|
|
{Name: "Program", Path: programDir, Extensions: []string{"exe"}, IsDeleted: true},
|
|
}
|
|
},
|
|
opts: &base.Options{},
|
|
res: singleFileResource("setup.exe"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "a category without a path is skipped",
|
|
cfg: func(cfg *base.DownloaderStoreConfig) {
|
|
cfg.Categories = []*base.DownloadCategory{
|
|
{Name: "Program", Path: " ", Extensions: []string{"exe"}},
|
|
}
|
|
},
|
|
opts: &base.Options{},
|
|
res: singleFileResource("setup.exe"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "a multi file resource keeps its layout",
|
|
opts: &base.Options{},
|
|
res: folderResource("bundle", "setup.exe", "readme.txt"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "a multi file resource with one match still keeps its layout",
|
|
opts: &base.Options{SelectFiles: []int{0}},
|
|
res: folderResource("bundle", "setup.exe", "readme.txt"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "a single file inside a folder resource is not routed",
|
|
opts: &base.Options{},
|
|
res: folderResource("bundle", "setup.exe"),
|
|
want: "./downloads",
|
|
},
|
|
{
|
|
name: "a missing resource keeps the base directory",
|
|
opts: &base.Options{},
|
|
res: nil,
|
|
want: "./downloads",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := newTestCategoryConfig()
|
|
if tt.cfg != nil {
|
|
tt.cfg(cfg)
|
|
}
|
|
d := newUnitCategoryDownloader(cfg)
|
|
got, err := d.decideDownloadDir(tt.opts, tt.res)
|
|
if err != nil {
|
|
t.Fatalf("decideDownloadDir() error = %v", err)
|
|
}
|
|
if got != tt.want {
|
|
t.Fatalf("decideDownloadDir() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDecideDownloadDirWhiteList(t *testing.T) {
|
|
defaultDir := filepath.ToSlash(t.TempDir())
|
|
categoryDir := defaultDir + "/Program"
|
|
newConfig := func() *base.DownloaderStoreConfig {
|
|
cfg := newTestCategoryConfig()
|
|
cfg.DownloadDir = defaultDir
|
|
cfg.Categories = []*base.DownloadCategory{
|
|
{Name: "Program", Path: categoryDir, Extensions: []string{"exe"}},
|
|
}
|
|
return cfg
|
|
}
|
|
|
|
t.Run("a rejected category directory falls back to the base directory", func(t *testing.T) {
|
|
d := newUnitCategoryDownloader(newConfig())
|
|
d.cfg.WhiteDownloadDirs = []string{defaultDir}
|
|
|
|
got, err := d.decideDownloadDir(&base.Options{Path: defaultDir}, singleFileResource("setup.exe"))
|
|
if err != nil {
|
|
t.Fatalf("expected white list fallback, got %v", err)
|
|
}
|
|
if got != defaultDir {
|
|
t.Fatalf("path = %q, want the base directory %q", got, defaultDir)
|
|
}
|
|
})
|
|
|
|
t.Run("a rejected base directory is an error", func(t *testing.T) {
|
|
d := newUnitCategoryDownloader(newConfig())
|
|
d.cfg.WhiteDownloadDirs = []string{defaultDir}
|
|
|
|
_, err := d.decideDownloadDir(&base.Options{Path: defaultDir + "/Other", SkipAutoCategorize: true}, singleFileResource("setup.exe"))
|
|
if err == nil {
|
|
t.Fatal("expected a white list error for an explicit directory")
|
|
}
|
|
})
|
|
|
|
t.Run("equivalent category paths still fall back to the allowed spelling", func(t *testing.T) {
|
|
cfg := newConfig()
|
|
cfg.Categories[0].Path = defaultDir + "/."
|
|
d := newUnitCategoryDownloader(cfg)
|
|
d.cfg.WhiteDownloadDirs = []string{defaultDir}
|
|
got, err := d.decideDownloadDir(&base.Options{Path: defaultDir}, singleFileResource("setup.exe"))
|
|
if err != nil || got != defaultDir {
|
|
t.Fatalf("decideDownloadDir() = %q, %v; want %q, nil", got, err, defaultDir)
|
|
}
|
|
})
|
|
|
|
t.Run("an empty base directory is an error", func(t *testing.T) {
|
|
cfg := newConfig()
|
|
cfg.DownloadDir = ""
|
|
d := newUnitCategoryDownloader(cfg)
|
|
|
|
_, err := d.decideDownloadDir(&base.Options{SkipAutoCategorize: true}, singleFileResource("setup.exe"))
|
|
if err == nil {
|
|
t.Fatal("expected an error for an empty download directory")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDecideGroupDownloadDirWhiteList(t *testing.T) {
|
|
parent := t.TempDir()
|
|
root := filepath.Join(parent, "folder")
|
|
nested := filepath.Join(root, "nested")
|
|
for _, test := range []struct {
|
|
name string
|
|
path string
|
|
group *TaskGroup
|
|
allowed string
|
|
wantErr bool
|
|
}{
|
|
{name: "group root", path: root, group: &TaskGroup{Path: root}, allowed: filepath.Join(parent, "*")},
|
|
{name: "nested member", path: nested, group: &TaskGroup{Path: root}, allowed: filepath.Join(parent, "*")},
|
|
{name: "ordinary nested task", path: nested, allowed: filepath.Join(parent, "*"), wantErr: true},
|
|
{name: "member outside group", path: filepath.Join(parent, "other"), group: &TaskGroup{Path: root}, allowed: filepath.Join(parent, "*"), wantErr: true},
|
|
{name: "group permission revoked", path: nested, group: &TaskGroup{Path: root}, allowed: nested, wantErr: true},
|
|
{name: "literal group name", path: filepath.Join(parent, "%year%", "nested"), group: &TaskGroup{Path: filepath.Join(parent, "%year%")}, allowed: filepath.Join(parent, "*")},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
d := newUnitCategoryDownloader(newTestCategoryConfig())
|
|
d.cfg.WhiteDownloadDirs = []string{test.allowed}
|
|
got, err := d.decideDownloadDirForTask(&base.Options{Path: test.path, SkipAutoCategorize: true}, singleFileResource("file.txt"), test.group)
|
|
if (err != nil) != test.wantErr {
|
|
t.Fatalf("decideDownloadDirForTask() = %q, %v; want error = %v", got, err, test.wantErr)
|
|
}
|
|
if err == nil && got != test.path {
|
|
t.Fatalf("path = %q, want %q", got, test.path)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSuggestedDownloadDir(t *testing.T) {
|
|
programDir := filepath.Join("./downloads", "Program")
|
|
|
|
tests := []struct {
|
|
name string
|
|
cfg func(cfg *base.DownloaderStoreConfig)
|
|
res *base.Resource
|
|
want string
|
|
}{
|
|
{name: "a matching single file is suggested", res: singleFileResource("setup.exe"), want: programDir},
|
|
{
|
|
name: "a padded category path matches the creation decision",
|
|
cfg: func(cfg *base.DownloaderStoreConfig) { cfg.Categories[2].Path = " " + programDir + " " },
|
|
res: singleFileResource("setup.exe"),
|
|
want: programDir,
|
|
},
|
|
{
|
|
name: "the switch off suggests nothing",
|
|
cfg: func(cfg *base.DownloaderStoreConfig) { cfg.AutoCategorize = util.BoolPtr(false) },
|
|
res: singleFileResource("setup.exe"),
|
|
want: "",
|
|
},
|
|
{name: "a multi file resource suggests nothing", res: folderResource("bundle", "setup.exe"), want: ""},
|
|
{name: "a folder resource suggests nothing", res: folderResource("bundle", "setup.exe", "a.txt"), want: ""},
|
|
{name: "an unknown extension suggests nothing", res: singleFileResource("archive.zip"), want: ""},
|
|
{name: "a missing resource suggests nothing", res: nil, want: ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg := newTestCategoryConfig()
|
|
if tt.cfg != nil {
|
|
tt.cfg(cfg)
|
|
}
|
|
d := newUnitCategoryDownloader(cfg)
|
|
if got := d.suggestedDownloadDir(tt.res); got != tt.want {
|
|
t.Fatalf("suggestedDownloadDir() = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("a rejected category directory suggests nothing", func(t *testing.T) {
|
|
dir := filepath.ToSlash(t.TempDir())
|
|
cfg := newTestCategoryConfig()
|
|
cfg.DownloadDir = dir
|
|
cfg.Categories = []*base.DownloadCategory{
|
|
{Name: "Program", Path: dir + "/Program", Extensions: []string{"exe"}},
|
|
}
|
|
d := newUnitCategoryDownloader(cfg)
|
|
d.cfg.WhiteDownloadDirs = []string{dir}
|
|
|
|
if got := d.suggestedDownloadDir(singleFileResource("setup.exe")); got != "" {
|
|
t.Fatalf("suggestedDownloadDir() = %q, want an empty suggestion", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestNormalizeCategoryExtensions(t *testing.T) {
|
|
tests := []struct {
|
|
raw []string
|
|
want []string
|
|
}{
|
|
{[]string{"exe, msi"}, []string{"exe", "msi"}},
|
|
{[]string{" mp3 ", "..exe", "MUSIC"}, []string{"mp3", "exe", "music"}},
|
|
{[]string{"zip", "ZIP", "zip"}, []string{"zip"}},
|
|
{[]string{" ", "..", ""}, nil},
|
|
{nil, nil},
|
|
}
|
|
for _, tt := range tests {
|
|
got := normalizeCategoryExtensions(tt.raw)
|
|
if len(got) != len(tt.want) {
|
|
t.Fatalf("normalizeCategoryExtensions(%q) = %q, want %q", tt.raw, got, tt.want)
|
|
}
|
|
for i := range got {
|
|
if got[i] != tt.want[i] {
|
|
t.Fatalf("normalizeCategoryExtensions(%q) = %q, want %q", tt.raw, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// startCategoryFileServer serves a nameless url whose real file name only
|
|
// shows up in the Content-Disposition header.
|
|
func startCategoryFileServer(t *testing.T, fileName string, data []byte) string {
|
|
t.Helper()
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/download", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Disposition", `attachment; filename="`+fileName+`"`)
|
|
w.Header().Set("Content-Type", "application/octet-stream")
|
|
http.ServeContent(w, r, fileName, time.Time{}, bytes.NewReader(data))
|
|
})
|
|
server := httptest.NewServer(mux)
|
|
t.Cleanup(server.Close)
|
|
return server.URL + "/download"
|
|
}
|
|
|
|
func newSetupCategoryDownloader(t *testing.T) *Downloader {
|
|
t.Helper()
|
|
downloader := NewDownloader(nil)
|
|
if err := downloader.Setup(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
downloader.Delete(nil, true)
|
|
downloader.Clear()
|
|
})
|
|
return downloader
|
|
}
|
|
|
|
func putCategoryConfig(t *testing.T, d *Downloader, downloadDir string, autoCategorize bool, extensions []string) string {
|
|
t.Helper()
|
|
categoryDir := filepath.Join(downloadDir, "Program")
|
|
cfg, err := d.GetConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg.DownloadDir = downloadDir
|
|
cfg.AutoCategorize = util.BoolPtr(autoCategorize)
|
|
cfg.Categories = []*base.DownloadCategory{
|
|
{Name: "Program", Path: categoryDir, Extensions: extensions},
|
|
}
|
|
if err := d.PutConfig(cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return categoryDir
|
|
}
|
|
|
|
// watchTaskDone subscribes to the done event before the task is created, so a
|
|
// fast task can not finish before the test starts listening.
|
|
func watchTaskDone(t *testing.T, d *Downloader) <-chan struct{} {
|
|
t.Helper()
|
|
done := make(chan struct{}, 1)
|
|
d.Listener(func(event *Event) {
|
|
if event.Key == EventKeyDone {
|
|
select {
|
|
case done <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
})
|
|
return done
|
|
}
|
|
|
|
func waitTaskDone(t *testing.T, done <-chan struct{}) {
|
|
t.Helper()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(30 * time.Second):
|
|
t.Fatal("task did not finish")
|
|
}
|
|
}
|
|
|
|
func TestDownloader_CreateDirectAutoCategorize(t *testing.T) {
|
|
// The url carries no extension, the category is matched on the file name
|
|
// of the resolved resource.
|
|
url := startCategoryFileServer(t, "setup.data", []byte("0123456789"))
|
|
downloadDir := t.TempDir()
|
|
downloader := newSetupCategoryDownloader(t)
|
|
categoryDir := putCategoryConfig(t, downloader, downloadDir, true, []string{"data"})
|
|
done := watchTaskDone(t, downloader)
|
|
|
|
taskID, err := downloader.CreateDirect(&base.Request{URL: url}, &base.Options{Path: downloadDir})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task := downloader.GetTask(taskID)
|
|
if task == nil {
|
|
t.Fatal("task not found")
|
|
}
|
|
waitTaskDone(t, done)
|
|
if task.Meta.Opts.Path != categoryDir {
|
|
t.Fatalf("task path = %q, want the category dir %q", task.Meta.Opts.Path, categoryDir)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(categoryDir, "setup.data")); err != nil {
|
|
t.Fatalf("categorized file missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDownloader_CreateDirectDoesNotMatchTheRename(t *testing.T) {
|
|
url := startCategoryFileServer(t, "setup.data", []byte("0123456789"))
|
|
downloadDir := t.TempDir()
|
|
downloader := newSetupCategoryDownloader(t)
|
|
putCategoryConfig(t, downloader, downloadDir, true, []string{"exe"})
|
|
done := watchTaskDone(t, downloader)
|
|
|
|
// opts.Name only renames the saved file, it never decides the category.
|
|
taskID, err := downloader.CreateDirect(&base.Request{URL: url}, &base.Options{
|
|
Path: downloadDir,
|
|
Name: "renamed.exe",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task := downloader.GetTask(taskID)
|
|
if task == nil {
|
|
t.Fatal("task not found")
|
|
}
|
|
waitTaskDone(t, done)
|
|
if task.Meta.Opts.Path != downloadDir {
|
|
t.Fatalf("task path = %q, want the base dir %q", task.Meta.Opts.Path, downloadDir)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(downloadDir, "renamed.exe")); err != nil {
|
|
t.Fatalf("renamed file missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDownloader_CreateDirectSkipAutoCategorize(t *testing.T) {
|
|
url := startCategoryFileServer(t, "setup.data", []byte("0123456789"))
|
|
downloadDir := t.TempDir()
|
|
chosenDir := t.TempDir()
|
|
downloader := newSetupCategoryDownloader(t)
|
|
putCategoryConfig(t, downloader, downloadDir, true, []string{"data"})
|
|
done := watchTaskDone(t, downloader)
|
|
|
|
taskID, err := downloader.CreateDirect(&base.Request{URL: url}, &base.Options{
|
|
Path: chosenDir,
|
|
SkipAutoCategorize: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task := downloader.GetTask(taskID)
|
|
if task == nil {
|
|
t.Fatal("task not found")
|
|
}
|
|
waitTaskDone(t, done)
|
|
if task.Meta.Opts.Path != chosenDir {
|
|
t.Fatalf("task path = %q, want the chosen dir %q", task.Meta.Opts.Path, chosenDir)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(chosenDir, "setup.data")); err != nil {
|
|
t.Fatalf("file missing in the chosen dir: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDownloader_CreateWithOptionsAutoCategorize(t *testing.T) {
|
|
url := startCategoryFileServer(t, "setup.data", []byte("0123456789"))
|
|
downloadDir := t.TempDir()
|
|
downloader := newSetupCategoryDownloader(t)
|
|
categoryDir := putCategoryConfig(t, downloader, downloadDir, true, []string{"data"})
|
|
done := watchTaskDone(t, downloader)
|
|
|
|
rr, err := downloader.Resolve(&base.Request{URL: url})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rr.SuggestedDownloadDir != categoryDir {
|
|
t.Fatalf("suggested dir = %q, want %q", rr.SuggestedDownloadDir, categoryDir)
|
|
}
|
|
taskID, err := downloader.CreateWithOptions(rr.ID, &base.Options{Path: downloadDir})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task := downloader.GetTask(taskID)
|
|
if task == nil {
|
|
t.Fatal("task not found")
|
|
}
|
|
waitTaskDone(t, done)
|
|
if task.Meta.Opts.Path != categoryDir {
|
|
t.Fatalf("task path = %q, want the category dir %q", task.Meta.Opts.Path, categoryDir)
|
|
}
|
|
}
|
|
|
|
func TestDownloader_CreateWithOptionsKeepsTheDefaultDirectory(t *testing.T) {
|
|
url := startCategoryFileServer(t, "setup.data", []byte("0123456789"))
|
|
downloadDir := t.TempDir()
|
|
downloader := newSetupCategoryDownloader(t)
|
|
putCategoryConfig(t, downloader, downloadDir, true, []string{"data"})
|
|
done := watchTaskDone(t, downloader)
|
|
|
|
rr, err := downloader.Resolve(&base.Request{URL: url})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
taskID, err := downloader.CreateWithOptions(rr.ID, &base.Options{
|
|
Path: downloadDir,
|
|
AsDefaultPath: true,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
waitTaskDone(t, done)
|
|
|
|
// The task lands in the category directory, but that must not become the
|
|
// global default download directory.
|
|
task := downloader.GetTask(taskID)
|
|
if task == nil {
|
|
t.Fatal("task not found")
|
|
}
|
|
if task.Meta.Opts.Path == downloadDir {
|
|
t.Fatalf("task path = %q, want the category dir", task.Meta.Opts.Path)
|
|
}
|
|
cfg, err := downloader.GetConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.DownloadDir != downloadDir {
|
|
t.Fatalf("default download dir = %q, want the base dir %q", cfg.DownloadDir, downloadDir)
|
|
}
|
|
}
|
|
|
|
func TestDownloader_ResolveSuggestsWithoutSideEffects(t *testing.T) {
|
|
url := startCategoryFileServer(t, "setup.data", []byte("0123456789"))
|
|
downloadDir := t.TempDir()
|
|
downloader := newSetupCategoryDownloader(t)
|
|
categoryDir := putCategoryConfig(t, downloader, downloadDir, true, []string{"data"})
|
|
|
|
rr, err := downloader.Resolve(&base.Request{URL: url})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rr.SuggestedDownloadDir != categoryDir {
|
|
t.Fatalf("suggested dir = %q, want %q", rr.SuggestedDownloadDir, categoryDir)
|
|
}
|
|
if _, err := os.Stat(categoryDir); !os.IsNotExist(err) {
|
|
t.Fatalf("resolve created the suggested directory: %v", err)
|
|
}
|
|
cfg, err := downloader.GetConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.DownloadDir != downloadDir {
|
|
t.Fatalf("resolve changed the download dir to %q", cfg.DownloadDir)
|
|
}
|
|
}
|
|
|
|
func TestDownloader_ResolveSuggestsNothingWhenTheSwitchIsOff(t *testing.T) {
|
|
url := startCategoryFileServer(t, "setup.data", []byte("0123456789"))
|
|
downloadDir := t.TempDir()
|
|
downloader := newSetupCategoryDownloader(t)
|
|
putCategoryConfig(t, downloader, downloadDir, false, []string{"data"})
|
|
|
|
rr, err := downloader.Resolve(&base.Request{URL: url})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rr.SuggestedDownloadDir != "" {
|
|
t.Fatalf("suggested dir = %q, want an empty suggestion", rr.SuggestedDownloadDir)
|
|
}
|
|
}
|
|
|
|
func TestDownloader_SetupMigratesLegacyExtraCategories(t *testing.T) {
|
|
storage := NewMemStorage()
|
|
if err := storage.Setup([]string{bucketConfig}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
legacy := &base.DownloaderStoreConfig{
|
|
DownloadDir: t.TempDir(),
|
|
Extra: map[string]any{
|
|
"downloadCategories": []any{
|
|
map[string]any{"name": "", "path": filepath.Join(t.TempDir(), "Music"), "isBuiltIn": true, "nameKey": "categoryMusic"},
|
|
},
|
|
},
|
|
}
|
|
if err := storage.Put(bucketConfig, "config", legacy); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
downloader := NewDownloader(&DownloaderConfig{Storage: storage, StorageDir: t.TempDir()})
|
|
if err := downloader.Setup(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
downloader.Delete(nil, true)
|
|
downloader.Clear()
|
|
}()
|
|
|
|
cfg, err := downloader.GetConfig()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(cfg.Categories) != 1 || cfg.Categories[0].NameKey != "categoryMusic" || len(cfg.Categories[0].Extensions) == 0 {
|
|
t.Fatalf("categories = %+v, want the migrated built-in with its default extensions", cfg.Categories)
|
|
}
|
|
if _, ok := cfg.Extra["downloadCategories"]; ok {
|
|
t.Fatal("in memory config still carries the legacy key")
|
|
}
|
|
|
|
// The cleared key has to be persisted, not only held in memory.
|
|
var stored base.DownloaderStoreConfig
|
|
exist, err := storage.Get(bucketConfig, "config", &stored)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !exist {
|
|
t.Fatal("stored config missing")
|
|
}
|
|
if _, ok := stored.Extra["downloadCategories"]; ok {
|
|
t.Fatal("stored config still carries the legacy key")
|
|
}
|
|
if len(stored.Categories) != 1 || stored.Categories[0].NameKey != "categoryMusic" {
|
|
t.Fatalf("stored categories = %+v, want the migrated list", stored.Categories)
|
|
}
|
|
}
|
|
|
|
// readOnlyConfigStorage refuses every config write, so the setup can not
|
|
// persist a migrated category list.
|
|
type readOnlyConfigStorage struct {
|
|
Storage
|
|
}
|
|
|
|
func (s *readOnlyConfigStorage) Put(bucket string, key string, v any) error {
|
|
if bucket == bucketConfig {
|
|
return errors.New("config storage is read only")
|
|
}
|
|
return s.Storage.Put(bucket, key, v)
|
|
}
|
|
|
|
func TestDownloader_SetupFailsWhenTheMigrationCanNotBePersisted(t *testing.T) {
|
|
storage := NewMemStorage()
|
|
if err := storage.Setup([]string{bucketConfig}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
legacy := &base.DownloaderStoreConfig{
|
|
DownloadDir: t.TempDir(),
|
|
Extra: map[string]any{
|
|
"downloadCategories": []any{
|
|
map[string]any{"name": "Music", "path": filepath.Join(t.TempDir(), "Music")},
|
|
},
|
|
},
|
|
}
|
|
if err := storage.Put(bucketConfig, "config", legacy); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// The migrated list can not be persisted, so the setup reports the write
|
|
// error instead of continuing with a category list that is lost on restart.
|
|
downloader := NewDownloader(&DownloaderConfig{Storage: &readOnlyConfigStorage{Storage: storage}, StorageDir: t.TempDir()})
|
|
if err := downloader.Setup(); err == nil {
|
|
t.Fatal("setup reported no error, want the failed config write")
|
|
}
|
|
}
|
|
|
|
// categoryMatchingCases is the shared extension rule table of the backend and
|
|
// the ui. The ui runs the same file, see
|
|
// ui/flutter/test/api/downloader_config_test.dart, so a rule that only changes
|
|
// on one side breaks the other side's test.
|
|
type categoryMatchingCases struct {
|
|
NormalizeExtensions []struct {
|
|
Input []string `json:"input"`
|
|
Expected []string `json:"expected"`
|
|
} `json:"normalizeExtensions"`
|
|
}
|
|
|
|
func TestSharedCategoryMatchingRules(t *testing.T) {
|
|
data, err := os.ReadFile(filepath.Join("testdata", "category_matching_cases.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var cases categoryMatchingCases
|
|
if err := json.Unmarshal(data, &cases); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range cases.NormalizeExtensions {
|
|
got := normalizeCategoryExtensions(c.Input)
|
|
if !slices.Equal(got, c.Expected) {
|
|
t.Errorf("normalizeCategoryExtensions(%q) = %q, want %q", c.Input, got, c.Expected)
|
|
}
|
|
}
|
|
}
|