package huaweidrive import ( "context" "errors" "fmt" "mime" "net/http" "path" "testing" "time" "github.com/rclone/rclone/backend/huaweidrive/api" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config/configmap" "github.com/rclone/rclone/fs/hash" "github.com/rclone/rclone/fstest/fstests" "github.com/rclone/rclone/lib/encoder" ) // TestIntegration runs integration tests against the remote func TestIntegration(t *testing.T) { fstests.Run(t, &fstests.Opt{ RemoteName: "TestHuaweiDrive:", NilObject: (*Object)(nil), SkipBadWindowsCharacters: true, }) } // TestNewFs tests the NewFs constructor func TestNewFs(t *testing.T) { ctx := context.Background() // Test with empty config - should fail due to missing OAuth credentials m := configmap.Simple{} _, err := NewFs(ctx, "test", "", m) if err == nil { t.Fatal("expected error with empty config") } } // TestFsName tests the filesystem name func TestFsName(t *testing.T) { f := &Fs{name: "test"} if f.Name() != "test" { t.Errorf("expected name 'test', got %q", f.Name()) } } // TestFsRoot tests the filesystem root func TestFsRoot(t *testing.T) { f := &Fs{root: "test/path"} if f.Root() != "test/path" { t.Errorf("expected root 'test/path', got %q", f.Root()) } } // TestFsString tests the filesystem string representation func TestFsString(t *testing.T) { f := &Fs{root: "test/path"} expected := "Huawei Drive root 'test/path'" if f.String() != expected { t.Errorf("expected string %q, got %q", expected, f.String()) } } // TestFsPrecision tests the filesystem precision func TestFsPrecision(t *testing.T) { f := &Fs{} precision := f.Precision() expectedPrecision := fs.ModTimeNotSupported if precision != expectedPrecision { t.Errorf("expected precision %v (ModTimeNotSupported), got %v", expectedPrecision, precision) } } // TestFsHashes tests the supported hash types func TestFsHashes(t *testing.T) { f := &Fs{} hashes := f.Hashes() if !hashes.Contains(hash.SHA256) { t.Error("expected SHA256 hash support") } } // TestModTimeSupport tests if the filesystem supports modification time preservation func TestModTimeSupport(t *testing.T) { f := &Fs{} // Huawei Drive does NOT preserve original modification times // Despite the API accepting editedTime/createdTime parameters, // the server always overwrites them with current server timestamp expectedPrecision := fs.ModTimeNotSupported if f.Precision() != expectedPrecision { t.Errorf("expected precision of %v to indicate no time preservation, got %v", expectedPrecision, f.Precision()) } } // TestTimeFormats tests time format handling for RFC 3339 func TestTimeFormats(t *testing.T) { // Test that we can format time correctly for Huawei Drive API testTime := time.Date(2023, 10, 15, 14, 30, 45, 0, time.UTC) formatted := testTime.Format(time.RFC3339) expected := "2023-10-15T14:30:45Z" if formatted != expected { t.Errorf("expected RFC3339 format %q, got %q", expected, formatted) } // Test parsing back parsed, err := time.Parse(time.RFC3339, formatted) if err != nil { t.Errorf("failed to parse RFC3339 time: %v", err) } if !parsed.Equal(testTime) { t.Errorf("parsed time %v does not match original %v", parsed, testTime) } } // TestEncoding tests filename encoding for Huawei Drive restrictions func TestEncoding(t *testing.T) { // Create encoder with Huawei Drive restrictions enc := encoder.MultiEncoder( //nolint:unconvert encoder.Display | encoder.EncodeBackSlash | encoder.EncodeSlash | encoder.EncodeInvalidUtf8 | encoder.EncodeRightSpace | encoder.EncodeLeftSpace | encoder.EncodeLeftTilde | encoder.EncodeRightPeriod | encoder.EncodeLeftPeriod | encoder.EncodeColon | encoder.EncodePipe | encoder.EncodeDoubleQuote | encoder.EncodeLtGt | encoder.EncodeQuestion | encoder.EncodeAsterisk | encoder.EncodeCtl | encoder.EncodeDot) // Test cases for problematic characters that should be encoded testCases := []struct { name string input string expected string }{ {"angle_brackets", "file<>name", "file<>name"}, {"quotes", "file\"name", "file"name"}, {"pipe", "file|name", "file|name"}, {"colon", "file:name", "file:name"}, {"asterisk", "file*name", "file*name"}, {"question", "file?name", "file?name"}, {"backslash", "file\\name", "file\name"}, {"forward_slash", "file/name", "file/name"}, {"leading_space", " filename", "␠filename"}, {"trailing_space", "filename ", "filename␠"}, {"leading_dot", ".filename", ".filename"}, {"control_chars", "file\tname\ntest", "file␉name␊test"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { encoded := enc.FromStandardName(tc.input) if encoded != tc.expected { t.Errorf("encoding %q: expected %q, got %q", tc.input, tc.expected, encoded) } // Test decoding back - only for reversible encodings // Some characters like forward slash and control chars are one-way encoded decoded := enc.ToStandardName(encoded) if tc.name != "forward_slash" && tc.name != "control_chars" { if decoded != tc.input { t.Errorf("decoding %q: expected %q, got %q", encoded, tc.input, decoded) } } }) } } // TestFileNameEncoding tests that problematic characters are properly encoded func TestFileNameEncoding(t *testing.T) { // Create a mock Fs with default encoding options opts := Options{} // Set the default encoding from our config opts.Enc = (encoder.Display | encoder.EncodeBackSlash | encoder.EncodeInvalidUtf8 | encoder.EncodeRightSpace | encoder.EncodeLeftSpace | encoder.EncodeLeftTilde | encoder.EncodeRightPeriod | encoder.EncodeLeftPeriod | encoder.EncodeColon | encoder.EncodePipe | encoder.EncodeDoubleQuote | encoder.EncodeLtGt | encoder.EncodeQuestion | encoder.EncodeAsterisk | encoder.EncodeCtl | encoder.EncodeDot) f := &Fs{opt: opts} // Test problematic characters that Huawei Drive rejects testCases := []struct { input string desc string }{ {`file.txt`, "angle brackets"}, {`file|name.txt`, "pipe character"}, {`file:name.txt`, "colon"}, {`file"name.txt`, "double quote"}, {`file*name.txt`, "asterisk"}, {`file?name.txt`, "question mark"}, {`file\name.txt`, "backslash"}, {` leading_space.txt`, "leading space"}, {`trailing_space.txt `, "trailing space"}, {`.leading_dot.txt`, "leading dot"}, {`trailing_dot.txt.`, "trailing dot"}, {`~leading_tilde.txt`, "leading tilde"}, {"file\x00name.txt", "control character"}, } for _, tc := range testCases { encoded := f.opt.Enc.FromStandardName(tc.input) // The encoded name should be different from input (meaning it was encoded) if encoded == tc.input { t.Errorf("Expected %s (%q) to be encoded, but got same string", tc.desc, tc.input) } // Test that we can decode it back (skip control characters as they are not reversible) decoded := f.opt.Enc.ToStandardName(encoded) if tc.desc != "control character" && decoded != tc.input { t.Errorf("Round-trip failed for %s: input=%q, encoded=%q, decoded=%q", tc.desc, tc.input, encoded, decoded) } t.Logf("✓ %s: %q → %q → %q", tc.desc, tc.input, encoded, decoded) } } // TestQueryFilter tests the QueryFilter functionality func TestQueryFilter(t *testing.T) { t.Run("empty_filter", func(t *testing.T) { filter := NewQueryFilter() if filter.String() != "" { t.Errorf("expected empty string, got %q", filter.String()) } }) t.Run("single_condition", func(t *testing.T) { filter := NewQueryFilter() filter.AddParentFolder("test_parent_id") expected := "'test_parent_id' in parentFolder" if filter.String() != expected { t.Errorf("expected %q, got %q", expected, filter.String()) } }) t.Run("multiple_conditions", func(t *testing.T) { filter := NewQueryFilter() filter.AddParentFolder("parent_id") filter.AddMimeType("image/jpeg") filter.AddFavorite(true) expected := "'parent_id' in parentFolder and mimeType = 'image/jpeg' and favorite = true" if filter.String() != expected { t.Errorf("expected %q, got %q", expected, filter.String()) } }) t.Run("filename_with_quotes", func(t *testing.T) { filter := NewQueryFilter() filter.AddFileName("file'with'quotes.txt") expected := "fileName = 'file\\'with\\'quotes.txt'" if filter.String() != expected { t.Errorf("expected %q, got %q", expected, filter.String()) } }) t.Run("mime_type_not_equal", func(t *testing.T) { filter := NewQueryFilter() filter.AddMimeTypeNot("application/vnd.huawei-apps.folder") expected := "mimeType != 'application/vnd.huawei-apps.folder'" if filter.String() != expected { t.Errorf("expected %q, got %q", expected, filter.String()) } }) t.Run("filename_contains", func(t *testing.T) { filter := NewQueryFilter() filter.AddFileNameContains("test") expected := "fileName contains 'test'" if filter.String() != expected { t.Errorf("expected %q, got %q", expected, filter.String()) } }) t.Run("recycled_status", func(t *testing.T) { filter := NewQueryFilter() filter.AddRecycled(true) filter.AddDirectlyRecycled(false) expected := "recycled = true and directlyRecycled = false" if filter.String() != expected { t.Errorf("expected %q, got %q", expected, filter.String()) } }) t.Run("edited_time_range", func(t *testing.T) { filter := NewQueryFilter() testTime := time.Date(2023, 10, 15, 14, 30, 45, 0, time.UTC) filter.AddEditedTimeRange(">=", testTime) expected := "editedTime >= '2023-10-15T14:30:45Z'" if filter.String() != expected { t.Errorf("expected %q, got %q", expected, filter.String()) } }) } // TestAPITypes tests the API type structures and methods func TestAPITypes(t *testing.T) { t.Run("file_is_dir", func(t *testing.T) { // Test folder folder := api.File{MimeType: api.FolderMimeType} if !folder.IsDir() { t.Error("expected folder to be recognized as directory") } // Test regular file file := api.File{MimeType: "text/plain"} if file.IsDir() { t.Error("expected regular file to not be recognized as directory") } }) t.Run("constants", func(t *testing.T) { if api.FolderMimeType != "application/vnd.huawei-apps.folder" { t.Errorf("unexpected folder mime type: %q", api.FolderMimeType) } if api.CategoryDriveFile != "drive#file" { t.Errorf("unexpected drive file category: %q", api.CategoryDriveFile) } if api.CategoryDriveFileList != "drive#fileList" { t.Errorf("unexpected drive file list category: %q", api.CategoryDriveFileList) } if api.UploadTypeMultipart != "multipart" { t.Errorf("unexpected multipart upload type: %q", api.UploadTypeMultipart) } }) } // TestErrorHandling tests error handling scenarios func TestErrorHandling(t *testing.T) { ctx := context.Background() testCases := []struct { name string statusCode int errorBody string expectErr bool errType string }{ {"missing_param_400", 400, "21004001", true, "missing required parameters"}, {"invalid_param_400", 400, "PARAM_INVALID", true, "invalid parameters"}, {"parent_not_found_400", 400, "PARENTFOLDER_NOT_FOUND", true, "ErrorDirNotFound"}, {"insufficient_scope_403", 403, "INSUFFICIENT_SCOPE", true, "insufficient OAuth scope"}, {"insufficient_permission_403", 403, "INSUFFICIENT_PERMISSION", true, "insufficient permissions"}, {"agreement_not_signed_403", 403, "AGREEMENT_NOT_SIGNED", true, "user agreement not signed"}, {"data_migrating_403", 403, "DATA_MIGRATING", false, ""}, // Should allow retry {"outer_service_error_403", 403, "OUTER_SERVICE_ERROR", false, ""}, // Should allow retry {"cursor_expired_410", 410, "CURSOR_EXPIRED", true, "cursor expired"}, {"temp_data_cleared_410", 410, "TEMP_DATA_CLEARED", true, "temporary data cleared"}, {"server_temp_error_500", 500, "SERVER_TEMP_ERROR", false, ""}, // Should allow retry {"outer_service_unavailable_500", 500, "OUTER_SERVICE_UNAVAILABLE", false, ""}, // Should allow retry } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Mock HTTP response resp := &http.Response{ StatusCode: tc.statusCode, } // Mock error with specific error code err := errors.New(tc.errorBody) shouldRetryResult, resultErr := shouldRetry(ctx, resp, err) if tc.expectErr { // Should not retry and should return error if shouldRetryResult { t.Error("expected shouldRetry to return false for non-retryable error") } if resultErr == nil { t.Error("expected error to be returned") } } else if !shouldRetryResult { // Should retry (temporary error) t.Error("expected shouldRetry to return true for retryable error") } }) } } // TestMimeTypeDetection tests MIME type detection for various file types func TestMimeTypeDetection(t *testing.T) { testCases := []struct { filename string expectedMimes []string // Allow multiple valid MIME types for cross-platform compatibility }{ {"document.pdf", []string{"application/pdf"}}, {"image.jpg", []string{"image/jpeg"}}, {"image.jpeg", []string{"image/jpeg"}}, {"image.png", []string{"image/png"}}, {"image.gif", []string{"image/gif"}}, {"video.mp4", []string{"video/mp4"}}, {"video.avi", []string{"video/x-msvideo", "video/vnd.avi", "video/avi"}}, // Different systems may return different values {"audio.mp3", []string{"audio/mpeg"}}, {"text.txt", []string{"text/plain; charset=utf-8"}}, {"data.json", []string{"application/json"}}, {"archive.zip", []string{"application/zip", "application/x-zip-compressed"}}, // Different systems may return different values {"noextension", []string{"application/octet-stream"}}, {"", []string{"application/octet-stream"}}, } for _, tc := range testCases { t.Run(tc.filename, func(t *testing.T) { detected := mime.TypeByExtension(path.Ext(tc.filename)) if detected == "" { detected = "application/octet-stream" } // Check if detected MIME type is in the list of expected types found := false for _, expected := range tc.expectedMimes { if detected == expected { found = true break } } if !found { t.Errorf("expected one of MIME types %v for %q, got %q", tc.expectedMimes, tc.filename, detected) } }) } } // TestRootFolderID tests root folder ID handling func TestRootFolderID(t *testing.T) { f := &Fs{} // Default root folder ID should be empty until detected if f.rootFolderID != "" { t.Errorf("expected empty root folder ID, got %q", f.rootFolderID) } // After setting, it should be accessible f.rootFolderID = "test-root-id" if f.rootFolderID != "test-root-id" { t.Errorf("expected root folder ID %q, got %q", "test-root-id", f.rootFolderID) } } // TestOptionsDefaults tests default option values func TestOptionsDefaults(t *testing.T) { opts := Options{} // Check default values match what we expect if opts.ChunkSize != 0 { t.Errorf("expected default ChunkSize to be 0, got %v", opts.ChunkSize) } if opts.ListChunk != 0 { t.Errorf("expected default ListChunk to be 0, got %v", opts.ListChunk) } if opts.UploadCutoff != 0 { t.Errorf("expected default UploadCutoff to be 0, got %v", opts.UploadCutoff) } } // TestParsePath tests path parsing functionality func TestParsePath(t *testing.T) { testCases := []struct { input string expected string }{ {"", ""}, {"/", ""}, {"path", "path"}, {"/path", "path"}, {"path/", "path"}, {"/path/", "path"}, {"path/to/file", "path/to/file"}, {"/path/to/file", "path/to/file"}, {"path/to/file/", "path/to/file"}, {"/path/to/file/", "path/to/file"}, {"//multiple//slashes//", "multiple//slashes"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("input_%q", tc.input), func(t *testing.T) { result := parsePath(tc.input) if result != tc.expected { t.Errorf("parsePath(%q): expected %q, got %q", tc.input, tc.expected, result) } }) } } // TestConstants tests various constants and configuration values func TestConstants(t *testing.T) { // Test OAuth configuration if rcloneClientID != "115505059" { t.Errorf("expected client ID %q, got %q", "115505059", rcloneClientID) } // Test URLs if rootURL != "https://driveapis.cloud.huawei.com.cn/drive/v1" { t.Errorf("expected root URL %q, got %q", "https://driveapis.cloud.huawei.com.cn/drive/v1", rootURL) } if uploadURL != "https://driveapis.cloud.huawei.com.cn/upload/drive/v1" { t.Errorf("expected upload URL %q, got %q", "https://driveapis.cloud.huawei.com.cn/upload/drive/v1", uploadURL) } // Test size limits expectedChunkSize := 8 * fs.Mebi // 8MB if defaultChunkSize != expectedChunkSize { t.Errorf("expected default chunk size %d, got %d", expectedChunkSize, defaultChunkSize) } } // TestFeatures tests the filesystem features configuration func TestFeatures(t *testing.T) { ctx := context.Background() f := &Fs{} // Create features manually since we can't call NewFs without proper config features := (&fs.Features{ CaseInsensitive: false, DuplicateFiles: false, ReadMimeType: true, WriteMimeType: true, CanHaveEmptyDirectories: true, BucketBased: false, ReadMetadata: true, WriteMetadata: true, UserMetadata: true, ReadDirMetadata: false, WriteDirMetadata: false, PartialUploads: false, NoMultiThreading: false, SlowModTime: false, SlowHash: false, }).Fill(ctx, f) f.features = features // Test that features are configured correctly for Huawei Drive if f.features.CaseInsensitive { t.Error("Huawei Drive should be case sensitive") } if f.features.DuplicateFiles { t.Error("Huawei Drive should not allow duplicate files") } if !f.features.ReadMimeType { t.Error("Huawei Drive should support reading MIME types") } if !f.features.WriteMimeType { t.Error("Huawei Drive should support setting MIME types") } if !f.features.CanHaveEmptyDirectories { t.Error("Huawei Drive should support empty directories") } if f.features.BucketBased { t.Error("Huawei Drive is not bucket-based") } if !f.features.ReadMetadata { t.Error("Huawei Drive should support reading metadata") } if !f.features.WriteMetadata { t.Error("Huawei Drive should support writing metadata") } } // TestAPIResponseParsing tests parsing of API responses func TestAPIResponseParsing(t *testing.T) { // Test About response parsing t.Run("about_response", func(t *testing.T) { about := api.About{ StorageQuota: struct { UsedSpace string `json:"usedSpace"` UserCapacity string `json:"userCapacity"` }{ UsedSpace: "1073741824", // 1GB UserCapacity: "10737418240", // 10GB }, MaxThumbnailSize: 2097152, // 2MB MaxFileUploadSize: "53687091200", // 50GB Domain: "driveapis.cloud.huawei.com.cn", Category: "drive#about", User: api.User{ PermissionID: "test_permission_id", DisplayName: "Test User", Me: true, Category: "drive#user", }, } // Test parsing if about.StorageQuota.UsedSpace != "1073741824" { t.Errorf("expected used space %q, got %q", "1073741824", about.StorageQuota.UsedSpace) } if about.User.Me != true { t.Error("expected user.me to be true") } if about.Category != api.CategoryDriveAbout { t.Errorf("expected category %q, got %q", api.CategoryDriveAbout, about.Category) } if about.MaxThumbnailSize != 2097152 { t.Errorf("expected max thumbnail size 2097152, got %d", about.MaxThumbnailSize) } if about.MaxFileUploadSize != "53687091200" { t.Errorf("expected max file upload size %q, got %q", "53687091200", about.MaxFileUploadSize) } if about.Domain != "driveapis.cloud.huawei.com.cn" { t.Errorf("expected domain %q, got %q", "driveapis.cloud.huawei.com.cn", about.Domain) } }) // Test FileList response parsing t.Run("filelist_response", func(t *testing.T) { fileList := api.FileList{ Files: []api.File{ { ID: "test_file_id", FileName: "test_file.txt", MimeType: "text/plain", Category: api.CategoryDriveFile, Size: 1024, SHA256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", CreatedTime: time.Now(), EditedTime: time.Now(), ParentFolder: []string{"parent_folder_id"}, Owners: []api.User{ { PermissionID: "owner_permission_id", DisplayName: "Owner User", Me: true, Category: "drive#user", }, }, OwnedByMe: true, EditedByMe: true, Recycled: false, Favorite: false, Containers: []string{"drive"}, }, }, NextPageToken: "next_page_token", Category: api.CategoryDriveFileList, } // Test file list properties if fileList.NextPageToken != "next_page_token" { t.Errorf("expected next page token %q, got %q", "next_page_token", fileList.NextPageToken) } if fileList.Category != api.CategoryDriveFileList { t.Errorf("expected category %q, got %q", api.CategoryDriveFileList, fileList.Category) } // Test file properties if len(fileList.Files) != 1 { t.Errorf("expected 1 file, got %d", len(fileList.Files)) } file := fileList.Files[0] if file.IsDir() { t.Error("text file should not be recognized as directory") } if file.Size != 1024 { t.Errorf("expected file size 1024, got %d", file.Size) } if !file.OwnedByMe { t.Error("expected file to be owned by me") } }) } // TestObjectMetadata tests object metadata operations func TestObjectMetadata(t *testing.T) { // Create a mock object with metadata obj := &Object{ fs: &Fs{}, remote: "test/file.txt", hasMetaData: true, size: 1024, modTime: time.Now(), id: "test_object_id", sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", mimeType: "text/plain", } // Test basic object properties if obj.Remote() != "test/file.txt" { t.Errorf("expected remote %q, got %q", "test/file.txt", obj.Remote()) } if obj.Size() != 1024 { t.Errorf("expected size 1024, got %d", obj.Size()) } // Test hash support ctx := context.Background() hash, err := obj.Hash(ctx, hash.SHA256) if err != nil { t.Errorf("unexpected error getting SHA256: %v", err) } if hash != obj.sha256 { t.Errorf("expected hash %q, got %q", obj.sha256, hash) } // Test unsupported hash (only SHA256 is supported by Huawei Drive) // We'll skip this test since it requires specific hash type constants // Test storable if !obj.Storable() { t.Error("object should be storable") } } // TestUploadTypes tests different upload type scenarios func TestUploadTypes(t *testing.T) { testCases := []struct { name string fileSize int64 cutoff fs.SizeSuffix expectType string }{ {"small_file_multipart", 1024, 20 * fs.Mebi, "multipart"}, {"large_file_resume", int64(100 * fs.Mebi), 20 * fs.Mebi, "resume"}, {"exact_cutoff_resume", int64(20 * fs.Mebi), 20 * fs.Mebi, "resume"}, {"zero_size_metadata", 0, 20 * fs.Mebi, "metadata"}, {"negative_size_resume", -1, 20 * fs.Mebi, "resume"}, // Unknown size } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var actualType string if tc.fileSize == 0 { actualType = "metadata" } else if tc.fileSize > 0 && tc.fileSize < int64(tc.cutoff) { actualType = "multipart" } else { actualType = "resume" } if actualType != tc.expectType { t.Errorf("expected upload type %q for size %d with cutoff %d, got %q", tc.expectType, tc.fileSize, tc.cutoff, actualType) } }) } } // TestRetryErrorCodes tests the retry error codes list func TestRetryErrorCodes(t *testing.T) { expectedCodes := []int{429, 500, 502, 503, 504, 509} if len(retryErrorCodes) != len(expectedCodes) { t.Errorf("expected %d retry error codes, got %d", len(expectedCodes), len(retryErrorCodes)) } for i, expected := range expectedCodes { if i >= len(retryErrorCodes) || retryErrorCodes[i] != expected { t.Errorf("expected retry error code %d at position %d, got %d", expected, i, retryErrorCodes[i]) } } } // TestEncodingConfiguration tests the encoding configuration matches requirements func TestEncodingConfiguration(t *testing.T) { // This should match the default encoding set in the config expectedEncoding := (encoder.Display | encoder.EncodeBackSlash | encoder.EncodeSlash | encoder.EncodeInvalidUtf8 | encoder.EncodeRightSpace | encoder.EncodeLeftSpace | encoder.EncodeLeftTilde | encoder.EncodeRightPeriod | encoder.EncodeLeftPeriod | encoder.EncodeColon | encoder.EncodePipe | encoder.EncodeDoubleQuote | encoder.EncodeLtGt | encoder.EncodeQuestion | encoder.EncodeAsterisk | encoder.EncodeCtl | encoder.EncodeDot) // Test that all required encodings are present requiredEncodings := []encoder.MultiEncoder{ encoder.EncodeBackSlash, // \ encoder.EncodeSlash, // / encoder.EncodeColon, // : encoder.EncodePipe, // | encoder.EncodeDoubleQuote, // " encoder.EncodeLtGt, // < > encoder.EncodeQuestion, // ? encoder.EncodeAsterisk, // * encoder.EncodeCtl, // Control characters } for _, required := range requiredEncodings { if (expectedEncoding & required) == 0 { t.Errorf("encoding configuration missing required encoding: %v", required) } } // Test that the encoding handles Huawei Drive restricted characters enc := expectedEncoding restrictedChars := []struct { char string desc string }{ {"<", "less than"}, {">", "greater than"}, {"|", "pipe"}, {":", "colon"}, {"\"", "double quote"}, {"*", "asterisk"}, {"?", "question mark"}, {"/", "forward slash"}, {"\\", "backslash"}, } for _, rc := range restrictedChars { testName := "test" + rc.char + "file.txt" encoded := enc.FromStandardName(testName) if encoded == testName { t.Errorf("character %s (%s) was not encoded", rc.char, rc.desc) } } } // TestScopeConfiguration tests OAuth scope configuration func TestScopeConfiguration(t *testing.T) { expectedScopes := []string{ "openid", "profile", "https://www.huawei.com/auth/drive", "https://www.huawei.com/auth/drive.file", } if len(oauthConfig.Scopes) != len(expectedScopes) { t.Errorf("expected %d OAuth scopes, got %d", len(expectedScopes), len(oauthConfig.Scopes)) } for i, expected := range expectedScopes { if i >= len(oauthConfig.Scopes) || oauthConfig.Scopes[i] != expected { t.Errorf("expected OAuth scope %q at position %d, got %q", expected, i, oauthConfig.Scopes[i]) } } // Test OAuth URLs expectedAuthURL := "https://oauth-login.cloud.huawei.com/oauth2/v3/authorize" if oauthConfig.AuthURL != expectedAuthURL { t.Errorf("expected auth URL %q, got %q", expectedAuthURL, oauthConfig.AuthURL) } expectedTokenURL := "https://oauth-login.cloud.huawei.com/oauth2/v3/token" if oauthConfig.TokenURL != expectedTokenURL { t.Errorf("expected token URL %q, got %q", expectedTokenURL, oauthConfig.TokenURL) } } // TestObjectString tests object string representation func TestObjectString(t *testing.T) { // Test normal object obj := &Object{remote: "path/to/file.txt"} expected := "path/to/file.txt" if obj.String() != expected { t.Errorf("expected object string %q, got %q", expected, obj.String()) } // Test nil object var nilObj *Object if nilObj.String() != "" { t.Errorf("expected nil object string %q, got %q", "", nilObj.String()) } } // TestSetModTime tests setting modification time func TestSetModTime(t *testing.T) { // Huawei Drive does not support setting modification times // SetModTime should always return fs.ErrorCantSetModTime obj := &Object{id: "test-id"} err := obj.SetModTime(context.Background(), time.Now()) if err != fs.ErrorCantSetModTime { t.Errorf("SetModTime should return fs.ErrorCantSetModTime, got: %v", err) } } // TestCleanUp tests the CleanUp functionality func TestCleanUp(t *testing.T) { // Test that the interface is correctly implemented at compile time // This test will fail to compile if CleanUpper interface is not properly implemented var _ fs.CleanUpper = (*Fs)(nil) t.Log("CleanUpper interface properly implemented") } // TestInterfaceCompliance tests all implemented interfaces at compile time func TestInterfaceCompliance(t *testing.T) { var _ fs.Fs = (*Fs)(nil) var _ fs.Copier = (*Fs)(nil) var _ fs.Mover = (*Fs)(nil) var _ fs.DirMover = (*Fs)(nil) var _ fs.ListRer = (*Fs)(nil) var _ fs.Abouter = (*Fs)(nil) var _ fs.Purger = (*Fs)(nil) var _ fs.CleanUpper = (*Fs)(nil) var _ fs.UserInfoer = (*Fs)(nil) var _ fs.Disconnecter = (*Fs)(nil) var _ fs.DirCacheFlusher = (*Fs)(nil) var _ fs.MimeTyper = (*Object)(nil) } // TestMimeTyper tests the MimeType method on Object func TestMimeTyper(t *testing.T) { ctx := context.Background() obj := &Object{ mimeType: "application/pdf", hasMetaData: true, } if obj.MimeType(ctx) != "application/pdf" { t.Errorf("expected MimeType %q, got %q", "application/pdf", obj.MimeType(ctx)) } folderObj := &Object{ mimeType: api.FolderMimeType, hasMetaData: true, } if folderObj.MimeType(ctx) != api.FolderMimeType { t.Errorf("expected folder MimeType %q, got %q", api.FolderMimeType, folderObj.MimeType(ctx)) } } // TestRootFolderIDConfig tests the root_folder_id configuration option func TestRootFolderIDConfig(t *testing.T) { opts := Options{ RootFolderID: "test-root-folder-id", } if opts.RootFolderID != "test-root-folder-id" { t.Errorf("expected RootFolderID %q, got %q", "test-root-folder-id", opts.RootFolderID) } emptyOpts := Options{} if emptyOpts.RootFolderID != "" { t.Errorf("expected empty default RootFolderID, got %q", emptyOpts.RootFolderID) } } // TestGlobalDomains tests the global domain detection func TestGlobalDomains(t *testing.T) { if !api.GlobalDomains["driveapis.cloud.huawei.com.cn"] { t.Error("driveapis.cloud.huawei.com.cn should be recognized as global domain") } if !api.GlobalDomains["drive.cloud.hicloud.com"] { t.Error("drive.cloud.hicloud.com should be recognized as global domain") } if api.GlobalDomains["unknown.domain.com"] { t.Error("unknown.domain.com should not be recognized as global domain") } } // TestDomainToRootURL tests domain-to-URL mapping func TestDomainToRootURL(t *testing.T) { testCases := []struct { domain string expected string }{ {"china", "https://drive-drcn.cloud.dbankcloud.cn"}, {"europe", "https://drive-dre.cloud.dbankcloud.cn"}, {"russia", "https://drive-drru.cloud.dbankcloud.cn"}, } for _, tc := range testCases { url, ok := api.DomainToRootURL[tc.domain] if !ok { t.Errorf("domain %q not found in DomainToRootURL", tc.domain) continue } if url != tc.expected { t.Errorf("domain %q: expected URL %q, got %q", tc.domain, tc.expected, url) } } } // TestDetectRootIDAlgorithm tests the root ID detection algorithm logic func TestDetectRootIDAlgorithm(t *testing.T) { t.Run("root_not_in_file_ids", func(t *testing.T) { files := []api.File{ {ID: "file1", ParentFolder: []string{"ROOT_ID"}}, {ID: "file2", ParentFolder: []string{"ROOT_ID"}}, {ID: "dir1", ParentFolder: []string{"ROOT_ID"}}, {ID: "file3", ParentFolder: []string{"dir1"}}, } fileIDs := make(map[string]bool) parentCounts := make(map[string]int) for _, f := range files { fileIDs[f.ID] = true for _, p := range f.ParentFolder { parentCounts[p]++ } } var bestID string var bestCount int for id, count := range parentCounts { if !fileIDs[id] && count > bestCount { bestID = id bestCount = count } } if bestID != "ROOT_ID" { t.Errorf("expected ROOT_ID, got %q", bestID) } if bestCount != 3 { t.Errorf("expected count 3, got %d", bestCount) } }) t.Run("all_parents_are_file_ids", func(t *testing.T) { files := []api.File{ {ID: "dir_a", ParentFolder: []string{"dir_b"}}, {ID: "dir_b", ParentFolder: []string{"dir_a"}}, } fileIDs := make(map[string]bool) parentCounts := make(map[string]int) for _, f := range files { fileIDs[f.ID] = true for _, p := range f.ParentFolder { parentCounts[p]++ } } var bestID string var bestCount int for id, count := range parentCounts { if !fileIDs[id] && count > bestCount { bestID = id bestCount = count } } if bestID != "" { t.Errorf("expected empty bestID from primary pass, got %q", bestID) } // Fallback for id, count := range parentCounts { if count > bestCount { bestID = id bestCount = count } } if bestID == "" { t.Error("expected fallback to find a parent ID") } }) t.Run("empty_file_list", func(t *testing.T) { files := []api.File{} if len(files) != 0 { t.Error("expected empty file list") } }) }