From cf07a5167a22e66761144902202cd38cfa57ebd7 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Fri, 26 Jun 2026 19:46:29 +0800 Subject: [PATCH] refactor storage upload tests by topic --- internal/service/storage_config_probe_test.go | 153 +++++++++ internal/service/storage_upload_jobs_test.go | 192 +++++++++++ internal/service/storage_upload_test.go | 324 ------------------ 3 files changed, 345 insertions(+), 324 deletions(-) create mode 100644 internal/service/storage_config_probe_test.go create mode 100644 internal/service/storage_upload_jobs_test.go diff --git a/internal/service/storage_config_probe_test.go b/internal/service/storage_config_probe_test.go new file mode 100644 index 0000000..a94c3c4 --- /dev/null +++ b/internal/service/storage_config_probe_test.go @@ -0,0 +1,153 @@ +package service + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestStorageConfigOpenListHTTPSAgainstHTTPHint(t *testing.T) { + openlist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(`{"code":200}`)) + })) + defer openlist.Close() + + _, storage := newStorageUploadTestService(t) + badHTTPS := "https://" + strings.TrimPrefix(openlist.URL, "http://") + err := storage.Test(t.Context(), StorageInput{ + Type: "openlist", + Config: map[string]any{ + "server": badHTTPS, + }, + }) + if err == nil { + t.Fatal("want protocol mismatch error") + } + if !strings.Contains(err.Error(), "请改用 http://") || !strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestStorageConfigOpenListTestRejectsUnauthorizedList(t *testing.T) { + openlist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/dav" { + t.Fatalf("unexpected openlist path %s", r.URL.Path) + } + w.WriteHeader(http.StatusUnauthorized) + _, _ = w.Write([]byte("unauthorized")) + })) + defer openlist.Close() + + _, storage := newStorageUploadTestService(t) + err := storage.Test(t.Context(), StorageInput{ + Type: "openlist", + Config: map[string]any{ + "server": openlist.URL, + }, + }) + if err == nil || !strings.Contains(err.Error(), "http 401") { + t.Fatalf("openlist unauthorized probe error = %v, want http 401", err) + } +} + +func TestStorageConfigOpenListTestUsesAPIListWithToken(t *testing.T) { + var listed bool + openlist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/fs/list" { + t.Fatalf("unexpected openlist path %s", r.URL.Path) + } + if r.Header.Get("Authorization") != "openlist-token" { + t.Fatalf("authorization = %q", r.Header.Get("Authorization")) + } + listed = true + _, _ = w.Write([]byte(`{"code":200,"data":{"content":[],"total":0}}`)) + })) + defer openlist.Close() + + _, storage := newStorageUploadTestService(t) + if err := storage.Test(t.Context(), StorageInput{ + Type: "openlist", + Config: map[string]any{ + "server": openlist.URL, + "token": "openlist-token", + }, + }); err != nil { + t.Fatalf("openlist API probe: %v", err) + } + if !listed { + t.Fatal("openlist test should probe /api/fs/list") + } +} + +func TestStorageConfigAlistTestRejectsUnauthorized(t *testing.T) { + alist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/me" { + t.Fatalf("unexpected alist path %s", r.URL.Path) + } + w.WriteHeader(http.StatusUnauthorized) + })) + defer alist.Close() + + _, storage := newStorageUploadTestService(t) + err := storage.Test(t.Context(), StorageInput{ + Type: "alist", + Config: map[string]any{ + "server": alist.URL, + }, + }) + if err == nil || !strings.Contains(err.Error(), "authentication failed") { + t.Fatalf("alist unauthorized probe error = %v, want authentication failed", err) + } +} + +func TestStorageConfigCloudProviderRejectsDisabledConfig(t *testing.T) { + _, storage := newStorageUploadTestService(t) + enabled := false + if _, err := storage.Save(t.Context(), StorageInput{ + Type: "openlist", + Config: map[string]any{ + "url": "http://127.0.0.1:5244/dav", + }, + Enabled: &enabled, + }); err != nil { + t.Fatal(err) + } + _, err := storage.CloudProvider(t.Context(), "openlist") + if err == nil || !strings.Contains(err.Error(), "disabled") { + t.Fatalf("disabled provider error = %v, want disabled", err) + } +} + +func TestStorageConfigSavePreservesExistingSecretWhenFormLeavesItBlank(t *testing.T) { + _, storage := newStorageUploadTestService(t) + if _, err := storage.Save(t.Context(), StorageInput{ + Type: "openlist", + Config: map[string]any{ + "server": "http://openlist.test", + "token": "openlist-token", + }, + }); err != nil { + t.Fatal(err) + } + if _, err := storage.Save(t.Context(), StorageInput{ + Type: "openlist", + Config: map[string]any{ + "server": "http://openlist.test", + "token": "", + "timeout_seconds": "180", + }, + }); err != nil { + t.Fatal(err) + } + view, err := storage.Get(t.Context(), "openlist") + if err != nil { + t.Fatal(err) + } + if view.Config["token"] != "openlist-token" { + t.Fatalf("token = %#v, want preserved token", view.Config["token"]) + } + if view.Config["timeout_seconds"] != "180" { + t.Fatalf("timeout_seconds = %#v, want updated timeout", view.Config["timeout_seconds"]) + } +} diff --git a/internal/service/storage_upload_jobs_test.go b/internal/service/storage_upload_jobs_test.go new file mode 100644 index 0000000..468c765 --- /dev/null +++ b/internal/service/storage_upload_jobs_test.go @@ -0,0 +1,192 @@ +package service + +import ( + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "testing" + + "go.uber.org/zap" +) + +func TestSchedulerCloudUploadUsesConfiguredLocalSource(t *testing.T) { + var uploaded []string + alist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/fs/mkdir": + _, _ = w.Write([]byte(`{"code":200}`)) + case "/api/fs/get": + w.WriteHeader(http.StatusNotFound) + case "/api/fs/put": + decoded, _ := url.PathUnescape(r.Header.Get("File-Path")) + uploaded = append(uploaded, decoded) + _, _ = w.Write([]byte(`{"code":200}`)) + default: + t.Fatalf("unexpected alist path %s", r.URL.Path) + } + })) + defer alist.Close() + + repos, storage := newStorageUploadTestService(t) + if _, err := storage.Save(t.Context(), StorageInput{ + Type: "alist", + Config: map[string]any{ + "server": alist.URL, + "token": "token", + "transfer_enabled": "true", + }, + }); err != nil { + t.Fatal(err) + } + source := t.TempDir() + if err := os.WriteFile(filepath.Join(source, "Show.S01E01.mkv"), []byte("episode"), 0o644); err != nil { + t.Fatal(err) + } + for key, value := range map[string]string{ + CloudUploadAutoEnabledKey: "true", + CloudUploadProviderKey: "alist", + CloudUploadSourceDirKey: source, + CloudUploadDestPathKey: "/cloud-media", + CloudUploadRecursiveKey: "true", + CloudUploadSidecarsKey: "false", + } { + if err := repos.Setting.Set(t.Context(), key, value); err != nil { + t.Fatal(err) + } + } + scheduler := NewSchedulerService(zap.NewNop(), repos, nil, nil, nil, storage, NewHub(zap.NewNop()), "") + if err := scheduler.jobUploadLocalToCloud(t.Context()); err != nil { + t.Fatalf("cloud upload job: %v", err) + } + if len(uploaded) != 1 || uploaded[0] != "/cloud-media/Show.S01E01.mkv" { + t.Fatalf("uploaded = %#v", uploaded) + } +} + +func TestStorageConfigUploadLocalToCloudDrive2(t *testing.T) { + var uploaded []string + dav := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "MKCOL": + w.WriteHeader(http.StatusCreated) + case http.MethodHead: + w.WriteHeader(http.StatusNotFound) + case http.MethodPut: + uploaded = append(uploaded, r.URL.Path) + w.WriteHeader(http.StatusCreated) + default: + t.Fatalf("unexpected method %s %s", r.Method, r.URL.Path) + } + })) + defer dav.Close() + + _, storage := newStorageUploadTestService(t) + if _, err := storage.Save(t.Context(), StorageInput{ + Type: "clouddrive2", + Config: map[string]any{ + "url": dav.URL + "/dav", + "username": "user", + "password": "pass", + "transfer_enabled": "true", + }, + }); err != nil { + t.Fatal(err) + } + source := t.TempDir() + if err := os.WriteFile(filepath.Join(source, "Movie.mkv"), []byte("movie"), 0o644); err != nil { + t.Fatal(err) + } + res, err := storage.UploadLocal(t.Context(), CloudUploadInput{ + Type: "clouddrive2", + SourcePath: source, + DestPath: "/MediaStationGo", + Recursive: true, + }) + if err != nil { + t.Fatalf("upload local: %v", err) + } + if res.Uploaded != 1 || len(uploaded) != 1 || uploaded[0] != "/dav/MediaStationGo/Movie.mkv" { + t.Fatalf("result = %+v uploaded=%#v", res, uploaded) + } +} + +func TestStorageConfigUploadLocalRequiresTransferEnabled(t *testing.T) { + _, storage := newStorageUploadTestService(t) + if _, err := storage.Save(t.Context(), StorageInput{ + Type: "alist", + Config: map[string]any{ + "server": "http://alist.test", + "token": "token", + }, + }); err != nil { + t.Fatal(err) + } + source := t.TempDir() + if err := os.WriteFile(filepath.Join(source, "Movie.mkv"), []byte("movie"), 0o644); err != nil { + t.Fatal(err) + } + _, err := storage.UploadLocal(t.Context(), CloudUploadInput{ + Type: "alist", + SourcePath: source, + DestPath: "/MediaStationGo", + Recursive: true, + }) + if err == nil || !strings.Contains(err.Error(), "transfer is disabled") { + t.Fatalf("upload error = %v, want transfer disabled", err) + } +} + +func TestStorageConfigUploadLocalMoveDeletesSourceAfterUpload(t *testing.T) { + var uploaded []string + alist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/fs/mkdir": + _, _ = w.Write([]byte(`{"code":200}`)) + case "/api/fs/get": + w.WriteHeader(http.StatusNotFound) + case "/api/fs/put": + decoded, _ := url.PathUnescape(r.Header.Get("File-Path")) + uploaded = append(uploaded, decoded) + _, _ = w.Write([]byte(`{"code":200}`)) + default: + t.Fatalf("unexpected alist path %s", r.URL.Path) + } + })) + defer alist.Close() + + _, storage := newStorageUploadTestService(t) + if _, err := storage.Save(t.Context(), StorageInput{ + Type: "alist", + Config: map[string]any{ + "server": alist.URL, + "token": "token", + "transfer_enabled": "true", + "transfer_mode": "move", + }, + }); err != nil { + t.Fatal(err) + } + source := t.TempDir() + file := filepath.Join(source, "Movie.mkv") + if err := os.WriteFile(file, []byte("movie"), 0o644); err != nil { + t.Fatal(err) + } + res, err := storage.UploadLocal(t.Context(), CloudUploadInput{ + Type: "alist", + SourcePath: source, + DestPath: "/MediaStationGo", + Recursive: true, + }) + if err != nil { + t.Fatalf("upload local move: %v", err) + } + if res.Uploaded != 1 || res.Moved != 1 || len(uploaded) != 1 { + t.Fatalf("result = %+v uploaded=%#v", res, uploaded) + } + if _, err := os.Stat(file); !os.IsNotExist(err) { + t.Fatalf("source should be removed after move upload, stat err=%v", err) + } +} diff --git a/internal/service/storage_upload_test.go b/internal/service/storage_upload_test.go index 8e37b63..d39036f 100644 --- a/internal/service/storage_upload_test.go +++ b/internal/service/storage_upload_test.go @@ -224,330 +224,6 @@ func TestStorageConfigUploadLocalToOpenListAPIWithUsernamePassword(t *testing.T) } } -func TestStorageConfigOpenListHTTPSAgainstHTTPHint(t *testing.T) { - openlist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(`{"code":200}`)) - })) - defer openlist.Close() - - _, storage := newStorageUploadTestService(t) - badHTTPS := "https://" + strings.TrimPrefix(openlist.URL, "http://") - err := storage.Test(t.Context(), StorageInput{ - Type: "openlist", - Config: map[string]any{ - "server": badHTTPS, - }, - }) - if err == nil { - t.Fatal("want protocol mismatch error") - } - if !strings.Contains(err.Error(), "请改用 http://") || !strings.Contains(err.Error(), "server gave HTTP response to HTTPS client") { - t.Fatalf("unexpected error: %v", err) - } -} - -func TestStorageConfigOpenListTestRejectsUnauthorizedList(t *testing.T) { - openlist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/dav" { - t.Fatalf("unexpected openlist path %s", r.URL.Path) - } - w.WriteHeader(http.StatusUnauthorized) - _, _ = w.Write([]byte("unauthorized")) - })) - defer openlist.Close() - - _, storage := newStorageUploadTestService(t) - err := storage.Test(t.Context(), StorageInput{ - Type: "openlist", - Config: map[string]any{ - "server": openlist.URL, - }, - }) - if err == nil || !strings.Contains(err.Error(), "http 401") { - t.Fatalf("openlist unauthorized probe error = %v, want http 401", err) - } -} - -func TestStorageConfigOpenListTestUsesAPIListWithToken(t *testing.T) { - var listed bool - openlist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/fs/list" { - t.Fatalf("unexpected openlist path %s", r.URL.Path) - } - if r.Header.Get("Authorization") != "openlist-token" { - t.Fatalf("authorization = %q", r.Header.Get("Authorization")) - } - listed = true - _, _ = w.Write([]byte(`{"code":200,"data":{"content":[],"total":0}}`)) - })) - defer openlist.Close() - - _, storage := newStorageUploadTestService(t) - if err := storage.Test(t.Context(), StorageInput{ - Type: "openlist", - Config: map[string]any{ - "server": openlist.URL, - "token": "openlist-token", - }, - }); err != nil { - t.Fatalf("openlist API probe: %v", err) - } - if !listed { - t.Fatal("openlist test should probe /api/fs/list") - } -} - -func TestStorageConfigAlistTestRejectsUnauthorized(t *testing.T) { - alist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/me" { - t.Fatalf("unexpected alist path %s", r.URL.Path) - } - w.WriteHeader(http.StatusUnauthorized) - })) - defer alist.Close() - - _, storage := newStorageUploadTestService(t) - err := storage.Test(t.Context(), StorageInput{ - Type: "alist", - Config: map[string]any{ - "server": alist.URL, - }, - }) - if err == nil || !strings.Contains(err.Error(), "authentication failed") { - t.Fatalf("alist unauthorized probe error = %v, want authentication failed", err) - } -} - -func TestStorageConfigCloudProviderRejectsDisabledConfig(t *testing.T) { - _, storage := newStorageUploadTestService(t) - enabled := false - if _, err := storage.Save(t.Context(), StorageInput{ - Type: "openlist", - Config: map[string]any{ - "url": "http://127.0.0.1:5244/dav", - }, - Enabled: &enabled, - }); err != nil { - t.Fatal(err) - } - _, err := storage.CloudProvider(t.Context(), "openlist") - if err == nil || !strings.Contains(err.Error(), "disabled") { - t.Fatalf("disabled provider error = %v, want disabled", err) - } -} - -func TestStorageConfigSavePreservesExistingSecretWhenFormLeavesItBlank(t *testing.T) { - _, storage := newStorageUploadTestService(t) - if _, err := storage.Save(t.Context(), StorageInput{ - Type: "openlist", - Config: map[string]any{ - "server": "http://openlist.test", - "token": "openlist-token", - }, - }); err != nil { - t.Fatal(err) - } - if _, err := storage.Save(t.Context(), StorageInput{ - Type: "openlist", - Config: map[string]any{ - "server": "http://openlist.test", - "token": "", - "timeout_seconds": "180", - }, - }); err != nil { - t.Fatal(err) - } - view, err := storage.Get(t.Context(), "openlist") - if err != nil { - t.Fatal(err) - } - if view.Config["token"] != "openlist-token" { - t.Fatalf("token = %#v, want preserved token", view.Config["token"]) - } - if view.Config["timeout_seconds"] != "180" { - t.Fatalf("timeout_seconds = %#v, want updated timeout", view.Config["timeout_seconds"]) - } -} - -func TestSchedulerCloudUploadUsesConfiguredLocalSource(t *testing.T) { - var uploaded []string - alist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/api/fs/mkdir": - _, _ = w.Write([]byte(`{"code":200}`)) - case "/api/fs/get": - w.WriteHeader(http.StatusNotFound) - case "/api/fs/put": - decoded, _ := url.PathUnescape(r.Header.Get("File-Path")) - uploaded = append(uploaded, decoded) - _, _ = w.Write([]byte(`{"code":200}`)) - default: - t.Fatalf("unexpected alist path %s", r.URL.Path) - } - })) - defer alist.Close() - - repos, storage := newStorageUploadTestService(t) - if _, err := storage.Save(t.Context(), StorageInput{ - Type: "alist", - Config: map[string]any{ - "server": alist.URL, - "token": "token", - "transfer_enabled": "true", - }, - }); err != nil { - t.Fatal(err) - } - source := t.TempDir() - if err := os.WriteFile(filepath.Join(source, "Show.S01E01.mkv"), []byte("episode"), 0o644); err != nil { - t.Fatal(err) - } - for key, value := range map[string]string{ - CloudUploadAutoEnabledKey: "true", - CloudUploadProviderKey: "alist", - CloudUploadSourceDirKey: source, - CloudUploadDestPathKey: "/cloud-media", - CloudUploadRecursiveKey: "true", - CloudUploadSidecarsKey: "false", - } { - if err := repos.Setting.Set(t.Context(), key, value); err != nil { - t.Fatal(err) - } - } - scheduler := NewSchedulerService(zap.NewNop(), repos, nil, nil, nil, storage, NewHub(zap.NewNop()), "") - if err := scheduler.jobUploadLocalToCloud(t.Context()); err != nil { - t.Fatalf("cloud upload job: %v", err) - } - if len(uploaded) != 1 || uploaded[0] != "/cloud-media/Show.S01E01.mkv" { - t.Fatalf("uploaded = %#v", uploaded) - } -} - -func TestStorageConfigUploadLocalToCloudDrive2(t *testing.T) { - var uploaded []string - dav := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "MKCOL": - w.WriteHeader(http.StatusCreated) - case http.MethodHead: - w.WriteHeader(http.StatusNotFound) - case http.MethodPut: - uploaded = append(uploaded, r.URL.Path) - w.WriteHeader(http.StatusCreated) - default: - t.Fatalf("unexpected method %s %s", r.Method, r.URL.Path) - } - })) - defer dav.Close() - - _, storage := newStorageUploadTestService(t) - if _, err := storage.Save(t.Context(), StorageInput{ - Type: "clouddrive2", - Config: map[string]any{ - "url": dav.URL + "/dav", - "username": "user", - "password": "pass", - "transfer_enabled": "true", - }, - }); err != nil { - t.Fatal(err) - } - source := t.TempDir() - if err := os.WriteFile(filepath.Join(source, "Movie.mkv"), []byte("movie"), 0o644); err != nil { - t.Fatal(err) - } - res, err := storage.UploadLocal(t.Context(), CloudUploadInput{ - Type: "clouddrive2", - SourcePath: source, - DestPath: "/MediaStationGo", - Recursive: true, - }) - if err != nil { - t.Fatalf("upload local: %v", err) - } - if res.Uploaded != 1 || len(uploaded) != 1 || uploaded[0] != "/dav/MediaStationGo/Movie.mkv" { - t.Fatalf("result = %+v uploaded=%#v", res, uploaded) - } -} - -func TestStorageConfigUploadLocalRequiresTransferEnabled(t *testing.T) { - _, storage := newStorageUploadTestService(t) - if _, err := storage.Save(t.Context(), StorageInput{ - Type: "alist", - Config: map[string]any{ - "server": "http://alist.test", - "token": "token", - }, - }); err != nil { - t.Fatal(err) - } - source := t.TempDir() - if err := os.WriteFile(filepath.Join(source, "Movie.mkv"), []byte("movie"), 0o644); err != nil { - t.Fatal(err) - } - _, err := storage.UploadLocal(t.Context(), CloudUploadInput{ - Type: "alist", - SourcePath: source, - DestPath: "/MediaStationGo", - Recursive: true, - }) - if err == nil || !strings.Contains(err.Error(), "transfer is disabled") { - t.Fatalf("upload error = %v, want transfer disabled", err) - } -} - -func TestStorageConfigUploadLocalMoveDeletesSourceAfterUpload(t *testing.T) { - var uploaded []string - alist := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/api/fs/mkdir": - _, _ = w.Write([]byte(`{"code":200}`)) - case "/api/fs/get": - w.WriteHeader(http.StatusNotFound) - case "/api/fs/put": - decoded, _ := url.PathUnescape(r.Header.Get("File-Path")) - uploaded = append(uploaded, decoded) - _, _ = w.Write([]byte(`{"code":200}`)) - default: - t.Fatalf("unexpected alist path %s", r.URL.Path) - } - })) - defer alist.Close() - - _, storage := newStorageUploadTestService(t) - if _, err := storage.Save(t.Context(), StorageInput{ - Type: "alist", - Config: map[string]any{ - "server": alist.URL, - "token": "token", - "transfer_enabled": "true", - "transfer_mode": "move", - }, - }); err != nil { - t.Fatal(err) - } - source := t.TempDir() - file := filepath.Join(source, "Movie.mkv") - if err := os.WriteFile(file, []byte("movie"), 0o644); err != nil { - t.Fatal(err) - } - res, err := storage.UploadLocal(t.Context(), CloudUploadInput{ - Type: "alist", - SourcePath: source, - DestPath: "/MediaStationGo", - Recursive: true, - }) - if err != nil { - t.Fatalf("upload local move: %v", err) - } - if res.Uploaded != 1 || res.Moved != 1 || len(uploaded) != 1 { - t.Fatalf("result = %+v uploaded=%#v", res, uploaded) - } - if _, err := os.Stat(file); !os.IsNotExist(err) { - t.Fatalf("source should be removed after move upload, stat err=%v", err) - } -} - func newStorageUploadTestService(t *testing.T) (*repository.Container, *StorageConfigService) { t.Helper() db := newServiceTestDB(t, &model.StorageConfig{}, &model.Setting{}, &model.Library{}, &model.Media{})