github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/onedrive/onedrive_internal_test.go (about) 1 package onedrive 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "testing" 8 "time" 9 10 _ "github.com/rclone/rclone/backend/local" 11 "github.com/rclone/rclone/backend/onedrive/api" 12 "github.com/rclone/rclone/fs" 13 "github.com/rclone/rclone/fs/operations" 14 "github.com/rclone/rclone/fstest" 15 "github.com/rclone/rclone/fstest/fstests" 16 "github.com/rclone/rclone/lib/random" 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/require" 19 "golang.org/x/exp/slices" // replace with slices after go1.21 is the minimum version 20 ) 21 22 // go test -timeout 30m -run ^TestIntegration/FsMkdir/FsPutFiles/Internal$ github.com/rclone/rclone/backend/onedrive -remote TestOneDrive:meta -v 23 // go test -timeout 30m -run ^TestIntegration/FsMkdir/FsPutFiles/Internal$ github.com/rclone/rclone/backend/onedrive -remote TestOneDriveBusiness:meta -v 24 // go run ./fstest/test_all -remotes TestOneDriveBusiness:meta,TestOneDrive:meta -verbose -maxtries 1 25 26 var ( 27 t1 = fstest.Time("2023-08-26T23:13:06.499999999Z") 28 t2 = fstest.Time("2020-02-29T12:34:56.789Z") 29 t3 = time.Date(1994, time.December, 24, 9+12, 0, 0, 525600, time.FixedZone("Eastern Standard Time", -5)) 30 ctx = context.Background() 31 content = "hello" 32 ) 33 34 const ( 35 testUserID = "ryan@contoso.com" // demo user from doc examples (can't share files with yourself) 36 // https://learn.microsoft.com/en-us/onedrive/developer/rest-api/api/driveitem_invite?view=odsp-graph-online#http-request-1 37 ) 38 39 // TestMain drives the tests 40 func TestMain(m *testing.M) { 41 fstest.TestMain(m) 42 } 43 44 // TestWritePermissions tests reading and writing permissions 45 func (f *Fs) TestWritePermissions(t *testing.T, r *fstest.Run) { 46 // setup 47 ctx, ci := fs.AddConfig(ctx) 48 ci.Metadata = true 49 _ = f.opt.MetadataPermissions.Set("read,write") 50 file1 := r.WriteFile(randomFilename(), content, t2) 51 52 // add a permission with "read" role 53 permissions := defaultPermissions(f.driveType) 54 permissions[0].Roles[0] = api.ReadRole 55 expectedMeta, actualMeta := f.putWithMeta(ctx, t, &file1, permissions) 56 f.compareMeta(t, expectedMeta, actualMeta, false) 57 expectedP, actualP := unmarshalPerms(t, expectedMeta["permissions"]), unmarshalPerms(t, actualMeta["permissions"]) 58 59 found, num := false, 0 60 foundCount := 0 61 for i, p := range actualP { 62 for _, identity := range p.GetGrantedToIdentities(f.driveType) { 63 if identity.User.DisplayName == testUserID { 64 // note: expected will always be element 0 here, but actual may be variable based on org settings 65 assert.Equal(t, expectedP[0].Roles, p.Roles) 66 found, num = true, i 67 foundCount++ 68 } 69 } 70 if f.driveType == driveTypePersonal { 71 if p.GetGrantedTo(f.driveType) != nil && p.GetGrantedTo(f.driveType).User != (api.Identity{}) && p.GetGrantedTo(f.driveType).User.ID == testUserID { // shows up in a different place on biz vs. personal 72 assert.Equal(t, expectedP[0].Roles, p.Roles) 73 found, num = true, i 74 foundCount++ 75 } 76 } 77 } 78 assert.True(t, found, fmt.Sprintf("no permission found with expected role (want: \n\n%v \n\ngot: \n\n%v\n\n)", indent(t, expectedMeta["permissions"]), indent(t, actualMeta["permissions"]))) 79 assert.Equal(t, 1, foundCount, "expected to find exactly 1 match") 80 81 // update it to "write" 82 permissions = actualP 83 permissions[num].Roles[0] = api.WriteRole 84 expectedMeta, actualMeta = f.putWithMeta(ctx, t, &file1, permissions) 85 f.compareMeta(t, expectedMeta, actualMeta, false) 86 if f.driveType != driveTypePersonal { 87 // zero out some things we expect to be different 88 expectedP, actualP = unmarshalPerms(t, expectedMeta["permissions"]), unmarshalPerms(t, actualMeta["permissions"]) 89 normalize(expectedP) 90 normalize(actualP) 91 expectedMeta.Set("permissions", marshalPerms(t, expectedP)) 92 actualMeta.Set("permissions", marshalPerms(t, actualP)) 93 } 94 assert.JSONEq(t, expectedMeta["permissions"], actualMeta["permissions"]) 95 96 // remove it 97 permissions[num] = nil 98 _, actualMeta = f.putWithMeta(ctx, t, &file1, permissions) 99 if f.driveType == driveTypePersonal { 100 perms, ok := actualMeta["permissions"] 101 assert.False(t, ok, fmt.Sprintf("permissions metadata key was unexpectedly found: %v", perms)) 102 return 103 } 104 _, actualP = unmarshalPerms(t, expectedMeta["permissions"]), unmarshalPerms(t, actualMeta["permissions"]) 105 106 found = false 107 var foundP *api.PermissionsType 108 for _, p := range actualP { 109 if p.GetGrantedTo(f.driveType) == nil || p.GetGrantedTo(f.driveType).User == (api.Identity{}) || p.GetGrantedTo(f.driveType).User.ID != testUserID { 110 continue 111 } 112 found = true 113 foundP = p 114 } 115 assert.False(t, found, fmt.Sprintf("permission was found but expected to be removed: %v", foundP)) 116 } 117 118 // TestUploadSinglePart tests reading/writing permissions using uploadSinglepart() 119 // This is only used when file size is exactly 0. 120 func (f *Fs) TestUploadSinglePart(t *testing.T, r *fstest.Run) { 121 content = "" 122 f.TestWritePermissions(t, r) 123 content = "hello" 124 } 125 126 // TestReadPermissions tests that no permissions are written when --onedrive-metadata-permissions has "read" but not "write" 127 func (f *Fs) TestReadPermissions(t *testing.T, r *fstest.Run) { 128 // setup 129 ctx, ci := fs.AddConfig(ctx) 130 ci.Metadata = true 131 file1 := r.WriteFile(randomFilename(), "hello", t2) 132 133 // try adding a permission without --onedrive-metadata-permissions -- should fail 134 // test that what we got before vs. after is the same 135 _ = f.opt.MetadataPermissions.Set("read") 136 _, expectedMeta := f.putWithMeta(ctx, t, &file1, []*api.PermissionsType{}) // return var intentionally switched here 137 permissions := defaultPermissions(f.driveType) 138 _, actualMeta := f.putWithMeta(ctx, t, &file1, permissions) 139 if f.driveType == driveTypePersonal { 140 perms, ok := actualMeta["permissions"] 141 assert.False(t, ok, fmt.Sprintf("permissions metadata key was unexpectedly found: %v", perms)) 142 return 143 } 144 assert.JSONEq(t, expectedMeta["permissions"], actualMeta["permissions"]) 145 } 146 147 // TestReadMetadata tests that all the read-only system properties are present and non-blank 148 func (f *Fs) TestReadMetadata(t *testing.T, r *fstest.Run) { 149 // setup 150 ctx, ci := fs.AddConfig(ctx) 151 ci.Metadata = true 152 file1 := r.WriteFile(randomFilename(), "hello", t2) 153 permissions := defaultPermissions(f.driveType) 154 155 _ = f.opt.MetadataPermissions.Set("read,write") 156 _, actualMeta := f.putWithMeta(ctx, t, &file1, permissions) 157 optionals := []string{"package-type", "shared-by-id", "shared-scope", "shared-time", "shared-owner-id"} // not always present 158 for k := range systemMetadataInfo { 159 if slices.Contains(optionals, k) { 160 continue 161 } 162 if k == "description" && f.driveType != driveTypePersonal { 163 continue // not supported 164 } 165 gotV, ok := actualMeta[k] 166 assert.True(t, ok, fmt.Sprintf("property is missing: %v", k)) 167 assert.NotEmpty(t, gotV, fmt.Sprintf("property is blank: %v", k)) 168 } 169 } 170 171 // TestDirectoryMetadata tests reading and writing modtime and other metadata and permissions for directories 172 func (f *Fs) TestDirectoryMetadata(t *testing.T, r *fstest.Run) { 173 // setup 174 ctx, ci := fs.AddConfig(ctx) 175 ci.Metadata = true 176 _ = f.opt.MetadataPermissions.Set("read,write") 177 permissions := defaultPermissions(f.driveType) 178 permissions[0].Roles[0] = api.ReadRole 179 180 expectedMeta := fs.Metadata{ 181 "mtime": t1.Format(timeFormatOut), 182 "btime": t2.Format(timeFormatOut), 183 "content-type": dirMimeType, 184 "description": "that is so meta!", 185 } 186 b, err := json.MarshalIndent(permissions, "", "\t") 187 assert.NoError(t, err) 188 expectedMeta.Set("permissions", string(b)) 189 190 compareDirMeta := func(expectedMeta, actualMeta fs.Metadata, ignoreID bool) { 191 f.compareMeta(t, expectedMeta, actualMeta, ignoreID) 192 193 // check that all required system properties are present 194 optionals := []string{"package-type", "shared-by-id", "shared-scope", "shared-time", "shared-owner-id"} // not always present 195 for k := range systemMetadataInfo { 196 if slices.Contains(optionals, k) { 197 continue 198 } 199 if k == "description" && f.driveType != driveTypePersonal { 200 continue // not supported 201 } 202 gotV, ok := actualMeta[k] 203 assert.True(t, ok, fmt.Sprintf("property is missing: %v", k)) 204 assert.NotEmpty(t, gotV, fmt.Sprintf("property is blank: %v", k)) 205 } 206 } 207 newDst, err := operations.MkdirMetadata(ctx, f, "subdir", expectedMeta) 208 assert.NoError(t, err) 209 require.NotNil(t, newDst) 210 assert.Equal(t, "subdir", newDst.Remote()) 211 212 actualMeta, err := fs.GetMetadata(ctx, newDst) 213 assert.NoError(t, err) 214 assert.NotNil(t, actualMeta) 215 compareDirMeta(expectedMeta, actualMeta, false) 216 217 // modtime 218 assert.Equal(t, t1.Truncate(f.Precision()), newDst.ModTime(ctx)) 219 // try changing it and re-check it 220 newDst, err = operations.SetDirModTime(ctx, f, newDst, "", t2) 221 assert.NoError(t, err) 222 assert.Equal(t, t2.Truncate(f.Precision()), newDst.ModTime(ctx)) 223 // ensure that f.DirSetModTime also works 224 err = f.DirSetModTime(ctx, "subdir", t3) 225 assert.NoError(t, err) 226 entries, err := f.List(ctx, "") 227 assert.NoError(t, err) 228 entries.ForDir(func(dir fs.Directory) { 229 if dir.Remote() == "subdir" { 230 assert.True(t, t3.Truncate(f.Precision()).Equal(dir.ModTime(ctx)), fmt.Sprintf("got %v", dir.ModTime(ctx))) 231 } 232 }) 233 234 // test updating metadata on existing dir 235 actualMeta, err = fs.GetMetadata(ctx, newDst) // get fresh info as we've been changing modtimes 236 assert.NoError(t, err) 237 expectedMeta = actualMeta 238 expectedMeta.Set("description", "metadata is fun!") 239 expectedMeta.Set("btime", t3.Format(timeFormatOut)) 240 expectedMeta.Set("mtime", t1.Format(timeFormatOut)) 241 expectedMeta.Set("content-type", dirMimeType) 242 perms := unmarshalPerms(t, expectedMeta["permissions"]) 243 perms[0].Roles[0] = api.WriteRole 244 b, err = json.MarshalIndent(perms, "", "\t") 245 assert.NoError(t, err) 246 expectedMeta.Set("permissions", string(b)) 247 248 newDst, err = operations.MkdirMetadata(ctx, f, "subdir", expectedMeta) 249 assert.NoError(t, err) 250 require.NotNil(t, newDst) 251 assert.Equal(t, "subdir", newDst.Remote()) 252 253 actualMeta, err = fs.GetMetadata(ctx, newDst) 254 assert.NoError(t, err) 255 assert.NotNil(t, actualMeta) 256 compareDirMeta(expectedMeta, actualMeta, false) 257 258 // test copying metadata from one dir to another 259 copiedDir, err := operations.CopyDirMetadata(ctx, f, nil, "subdir2", newDst) 260 assert.NoError(t, err) 261 require.NotNil(t, copiedDir) 262 assert.Equal(t, "subdir2", copiedDir.Remote()) 263 264 actualMeta, err = fs.GetMetadata(ctx, copiedDir) 265 assert.NoError(t, err) 266 assert.NotNil(t, actualMeta) 267 compareDirMeta(expectedMeta, actualMeta, true) 268 269 // test DirModTimeUpdatesOnWrite 270 expectedTime := copiedDir.ModTime(ctx) 271 assert.True(t, !expectedTime.IsZero()) 272 r.WriteObject(ctx, copiedDir.Remote()+"/"+randomFilename(), "hi there", t3) 273 entries, err = f.List(ctx, "") 274 assert.NoError(t, err) 275 entries.ForDir(func(dir fs.Directory) { 276 if dir.Remote() == copiedDir.Remote() { 277 assert.True(t, expectedTime.Equal(dir.ModTime(ctx)), fmt.Sprintf("want %v got %v", expectedTime, dir.ModTime(ctx))) 278 } 279 }) 280 } 281 282 // TestServerSideCopyMove tests server-side Copy and Move 283 func (f *Fs) TestServerSideCopyMove(t *testing.T, r *fstest.Run) { 284 // setup 285 ctx, ci := fs.AddConfig(ctx) 286 ci.Metadata = true 287 _ = f.opt.MetadataPermissions.Set("read,write") 288 file1 := r.WriteFile(randomFilename(), content, t2) 289 290 // add a permission with "read" role 291 permissions := defaultPermissions(f.driveType) 292 permissions[0].Roles[0] = api.ReadRole 293 expectedMeta, actualMeta := f.putWithMeta(ctx, t, &file1, permissions) 294 f.compareMeta(t, expectedMeta, actualMeta, false) 295 296 comparePerms := func(expectedMeta, actualMeta fs.Metadata) (newExpectedMeta, newActualMeta fs.Metadata) { 297 expectedP, actualP := unmarshalPerms(t, expectedMeta["permissions"]), unmarshalPerms(t, actualMeta["permissions"]) 298 normalize(expectedP) 299 normalize(actualP) 300 expectedMeta.Set("permissions", marshalPerms(t, expectedP)) 301 actualMeta.Set("permissions", marshalPerms(t, actualP)) 302 assert.JSONEq(t, expectedMeta["permissions"], actualMeta["permissions"]) 303 return expectedMeta, actualMeta 304 } 305 306 // Copy 307 obj1, err := f.NewObject(ctx, file1.Path) 308 assert.NoError(t, err) 309 originalMeta := actualMeta 310 obj2, err := f.Copy(ctx, obj1, randomFilename()) 311 assert.NoError(t, err) 312 actualMeta, err = fs.GetMetadata(ctx, obj2) 313 assert.NoError(t, err) 314 expectedMeta, actualMeta = comparePerms(originalMeta, actualMeta) 315 f.compareMeta(t, expectedMeta, actualMeta, true) 316 317 // Move 318 obj3, err := f.Move(ctx, obj1, randomFilename()) 319 assert.NoError(t, err) 320 actualMeta, err = fs.GetMetadata(ctx, obj3) 321 assert.NoError(t, err) 322 expectedMeta, actualMeta = comparePerms(originalMeta, actualMeta) 323 f.compareMeta(t, expectedMeta, actualMeta, true) 324 } 325 326 // TestMetadataMapper tests adding permissions with the --metadata-mapper 327 func (f *Fs) TestMetadataMapper(t *testing.T, r *fstest.Run) { 328 // setup 329 ctx, ci := fs.AddConfig(ctx) 330 ci.Metadata = true 331 _ = f.opt.MetadataPermissions.Set("read,write") 332 file1 := r.WriteFile(randomFilename(), content, t2) 333 334 blob := `{"Metadata":{"permissions":"[{\"grantedToIdentities\":[{\"user\":{\"id\":\"ryan@contoso.com\"}}],\"roles\":[\"read\"]}]"}}` 335 if f.driveType != driveTypePersonal { 336 blob = `{"Metadata":{"permissions":"[{\"grantedToIdentitiesV2\":[{\"user\":{\"id\":\"ryan@contoso.com\"}}],\"roles\":[\"read\"]}]"}}` 337 } 338 339 // Copy 340 ci.MetadataMapper = []string{"echo", blob} 341 require.NoError(t, ci.Dump.Set("mapper")) 342 obj1, err := r.Flocal.NewObject(ctx, file1.Path) 343 assert.NoError(t, err) 344 obj2, err := operations.Copy(ctx, f, nil, randomFilename(), obj1) 345 assert.NoError(t, err) 346 actualMeta, err := fs.GetMetadata(ctx, obj2) 347 assert.NoError(t, err) 348 349 actualP := unmarshalPerms(t, actualMeta["permissions"]) 350 found := false 351 foundCount := 0 352 for _, p := range actualP { 353 for _, identity := range p.GetGrantedToIdentities(f.driveType) { 354 if identity.User.DisplayName == testUserID { 355 assert.Equal(t, []api.Role{api.ReadRole}, p.Roles) 356 found = true 357 foundCount++ 358 } 359 } 360 if f.driveType == driveTypePersonal { 361 if p.GetGrantedTo(f.driveType) != nil && p.GetGrantedTo(f.driveType).User != (api.Identity{}) && p.GetGrantedTo(f.driveType).User.ID == testUserID { // shows up in a different place on biz vs. personal 362 assert.Equal(t, []api.Role{api.ReadRole}, p.Roles) 363 found = true 364 foundCount++ 365 } 366 } 367 } 368 assert.True(t, found, fmt.Sprintf("no permission found with expected role (want: \n\n%v \n\ngot: \n\n%v\n\n)", blob, actualMeta)) 369 assert.Equal(t, 1, foundCount, "expected to find exactly 1 match") 370 } 371 372 // helper function to put an object with metadata and permissions 373 func (f *Fs) putWithMeta(ctx context.Context, t *testing.T, file *fstest.Item, perms []*api.PermissionsType) (expectedMeta, actualMeta fs.Metadata) { 374 t.Helper() 375 expectedMeta = fs.Metadata{ 376 "mtime": t1.Format(timeFormatOut), 377 "btime": t2.Format(timeFormatOut), 378 "description": "that is so meta!", 379 } 380 381 expectedMeta.Set("permissions", marshalPerms(t, perms)) 382 obj := fstests.PutTestContentsMetadata(ctx, t, f, file, content, true, "plain/text", expectedMeta) 383 do, ok := obj.(fs.Metadataer) 384 require.True(t, ok) 385 actualMeta, err := do.Metadata(ctx) 386 require.NoError(t, err) 387 return expectedMeta, actualMeta 388 } 389 390 func randomFilename() string { 391 return "some file-" + random.String(8) + ".txt" 392 } 393 394 func (f *Fs) compareMeta(t *testing.T, expectedMeta, actualMeta fs.Metadata, ignoreID bool) { 395 t.Helper() 396 for k, v := range expectedMeta { 397 gotV, ok := actualMeta[k] 398 switch k { 399 case "shared-owner-id", "shared-time", "shared-by-id", "shared-scope": 400 continue 401 case "permissions": 402 continue 403 case "utime": 404 assert.True(t, ok, fmt.Sprintf("expected metadata key is missing: %v", k)) 405 if f.driveType == driveTypePersonal { 406 compareTimeStrings(t, k, v, gotV, time.Minute) // read-only upload time, so slight difference expected -- use larger precision 407 continue 408 } 409 compareTimeStrings(t, k, expectedMeta["btime"], gotV, time.Minute) // another bizarre difference between personal and business... 410 continue 411 case "id": 412 if ignoreID { 413 continue // different id is expected when copying meta from one item to another 414 } 415 case "mtime", "btime": 416 assert.True(t, ok, fmt.Sprintf("expected metadata key is missing: %v", k)) 417 compareTimeStrings(t, k, v, gotV, time.Second) 418 continue 419 case "description": 420 if f.driveType != driveTypePersonal { 421 continue // not supported 422 } 423 } 424 assert.True(t, ok, fmt.Sprintf("expected metadata key is missing: %v", k)) 425 assert.Equal(t, v, gotV, actualMeta) 426 } 427 } 428 429 func compareTimeStrings(t *testing.T, remote, want, got string, precision time.Duration) { 430 wantT, err := time.Parse(timeFormatIn, want) 431 assert.NoError(t, err) 432 gotT, err := time.Parse(timeFormatIn, got) 433 assert.NoError(t, err) 434 fstest.AssertTimeEqualWithPrecision(t, remote, wantT, gotT, precision) 435 } 436 437 func marshalPerms(t *testing.T, p []*api.PermissionsType) string { 438 b, err := json.MarshalIndent(p, "", "\t") 439 assert.NoError(t, err) 440 return string(b) 441 } 442 443 func unmarshalPerms(t *testing.T, perms string) (p []*api.PermissionsType) { 444 t.Helper() 445 err := json.Unmarshal([]byte(perms), &p) 446 assert.NoError(t, err) 447 return p 448 } 449 450 func indent(t *testing.T, s string) string { 451 p := unmarshalPerms(t, s) 452 return marshalPerms(t, p) 453 } 454 455 func defaultPermissions(driveType string) []*api.PermissionsType { 456 if driveType == driveTypePersonal { 457 return []*api.PermissionsType{{ 458 GrantedTo: &api.IdentitySet{User: api.Identity{}}, 459 GrantedToIdentities: []*api.IdentitySet{{User: api.Identity{ID: testUserID}}}, 460 Roles: []api.Role{api.WriteRole}, 461 }} 462 } 463 return []*api.PermissionsType{{ 464 GrantedToV2: &api.IdentitySet{User: api.Identity{}}, 465 GrantedToIdentitiesV2: []*api.IdentitySet{{User: api.Identity{ID: testUserID}}}, 466 Roles: []api.Role{api.WriteRole}, 467 }} 468 } 469 470 // zeroes out some things we expect to be different when copying/moving between objects 471 func normalize(Ps []*api.PermissionsType) { 472 for _, ep := range Ps { 473 ep.ID = "" 474 ep.Link = nil 475 ep.ShareID = "" 476 } 477 } 478 479 func (f *Fs) resetTestDefaults(r *fstest.Run) { 480 ci := fs.GetConfig(ctx) 481 ci.Metadata = false 482 _ = f.opt.MetadataPermissions.Set("off") 483 r.Finalise() 484 } 485 486 // InternalTest dispatches all internal tests 487 func (f *Fs) InternalTest(t *testing.T) { 488 newTestF := func() (*Fs, *fstest.Run) { 489 r := fstest.NewRunIndividual(t) 490 testF, ok := r.Fremote.(*Fs) 491 if !ok { 492 t.FailNow() 493 } 494 return testF, r 495 } 496 497 testF, r := newTestF() 498 t.Run("TestWritePermissions", func(t *testing.T) { testF.TestWritePermissions(t, r) }) 499 testF.resetTestDefaults(r) 500 testF, r = newTestF() 501 t.Run("TestUploadSinglePart", func(t *testing.T) { testF.TestUploadSinglePart(t, r) }) 502 testF.resetTestDefaults(r) 503 testF, r = newTestF() 504 t.Run("TestReadPermissions", func(t *testing.T) { testF.TestReadPermissions(t, r) }) 505 testF.resetTestDefaults(r) 506 testF, r = newTestF() 507 t.Run("TestReadMetadata", func(t *testing.T) { testF.TestReadMetadata(t, r) }) 508 testF.resetTestDefaults(r) 509 testF, r = newTestF() 510 t.Run("TestDirectoryMetadata", func(t *testing.T) { testF.TestDirectoryMetadata(t, r) }) 511 testF.resetTestDefaults(r) 512 testF, r = newTestF() 513 t.Run("TestServerSideCopyMove", func(t *testing.T) { testF.TestServerSideCopyMove(t, r) }) 514 testF.resetTestDefaults(r) 515 t.Run("TestMetadataMapper", func(t *testing.T) { testF.TestMetadataMapper(t, r) }) 516 testF.resetTestDefaults(r) 517 } 518 519 var _ fstests.InternalTester = (*Fs)(nil)