github.com/jstaf/onedriver@v0.14.2-0.20240420231225-f07678f9e6ef/fs/delta_test.go (about) 1 // Run tests to verify that we are syncing changes from the server. 2 package fs 3 4 import ( 5 "bytes" 6 "context" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "testing" 11 "time" 12 13 "github.com/hanwen/go-fuse/v2/fuse" 14 "github.com/jstaf/onedriver/fs/graph" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 ) 18 19 // a helper function for use with tests 20 func (i *Inode) setContent(f *Filesystem, newContent []byte) { 21 i.DriveItem.Size = uint64(len(newContent)) 22 now := time.Now() 23 i.DriveItem.ModTime = &now 24 25 f.content.Insert(i.ID(), newContent) 26 27 if i.DriveItem.File == nil { 28 i.DriveItem.File = &graph.File{} 29 } 30 31 i.DriveItem.File.Hashes.QuickXorHash = graph.QuickXORHash(&newContent) 32 } 33 34 // In this test, we create a directory through the API, and wait to see if 35 // the cache picks it up post-creation. 36 func TestDeltaMkdir(t *testing.T) { 37 t.Parallel() 38 parent, err := graph.GetItemPath("/onedriver_tests/delta", auth) 39 require.NoError(t, err) 40 41 // create the directory directly through the API and bypass the cache 42 _, err = graph.Mkdir("first", parent.ID, auth) 43 require.NoError(t, err) 44 fname := filepath.Join(DeltaDir, "first") 45 46 // give the delta thread time to fetch the item 47 assert.Eventuallyf(t, func() bool { 48 st, err := os.Stat(fname) 49 if err == nil { 50 if st.Mode().IsDir() { 51 return true 52 } 53 t.Fatalf("%s was not a directory", fname) 54 } 55 return false 56 }, retrySeconds, time.Second, "%s not found", fname) 57 } 58 59 // We create a directory through the cache, then delete through the API and see 60 // if the cache picks it up. 61 func TestDeltaRmdir(t *testing.T) { 62 t.Parallel() 63 fname := filepath.Join(DeltaDir, "delete_me") 64 require.NoError(t, os.Mkdir(fname, 0755)) 65 66 item, err := graph.GetItemPath("/onedriver_tests/delta/delete_me", auth) 67 require.NoError(t, err) 68 require.NoError(t, graph.Remove(item.ID, auth)) 69 70 // wait for delta sync 71 assert.Eventually(t, func() bool { 72 _, err := os.Stat(fname) 73 return err == nil 74 }, retrySeconds, time.Second, "File deletion not picked up by client") 75 } 76 77 // Create a file locally, then rename it remotely and verify that the renamed 78 // file still has the correct content under the new parent. 79 func TestDeltaRename(t *testing.T) { 80 t.Parallel() 81 require.NoError(t, ioutil.WriteFile( 82 filepath.Join(DeltaDir, "delta_rename_start"), 83 []byte("cheesecake"), 84 0644, 85 )) 86 87 var item *graph.DriveItem 88 var err error 89 require.Eventually(t, func() bool { 90 item, err = graph.GetItemPath("/onedriver_tests/delta/delta_rename_start", auth) 91 return err == nil 92 }, 10*time.Second, time.Second, "Could not prepare /onedriver_test/delta/delta_rename_start") 93 inode := NewInodeDriveItem(item) 94 95 require.NoError(t, graph.Rename(inode.ID(), "delta_rename_end", inode.ParentID(), auth)) 96 fpath := filepath.Join(DeltaDir, "delta_rename_end") 97 assert.Eventually(t, func() bool { 98 if _, err := os.Stat(fpath); err == nil { 99 content, err := ioutil.ReadFile(fpath) 100 require.NoError(t, err) 101 return bytes.Contains(content, []byte("cheesecake")) 102 } 103 return false 104 }, retrySeconds, time.Second, "Rename not detected by client.") 105 } 106 107 // Create a file locally, then move it on the server to a new directory. Check 108 // to see if the cache picks it up. 109 func TestDeltaMoveParent(t *testing.T) { 110 t.Parallel() 111 require.NoError(t, ioutil.WriteFile( 112 filepath.Join(DeltaDir, "delta_move_start"), 113 []byte("carrotcake"), 114 0644, 115 )) 116 time.Sleep(time.Second) 117 118 var item *graph.DriveItem 119 var err error 120 require.Eventually(t, func() bool { 121 item, err = graph.GetItemPath("/onedriver_tests/delta/delta_move_start", auth) 122 return err == nil 123 }, 10*time.Second, time.Second) 124 125 newParent, err := graph.GetItemPath("/onedriver_tests/", auth) 126 require.NoError(t, err) 127 128 require.NoError(t, graph.Rename(item.ID, "delta_rename_end", newParent.ID, auth)) 129 fpath := filepath.Join(TestDir, "delta_rename_end") 130 assert.Eventually(t, func() bool { 131 if _, err := os.Stat(fpath); err == nil { 132 content, err := ioutil.ReadFile(fpath) 133 require.NoError(t, err) 134 return bytes.Contains(content, []byte("carrotcake")) 135 } 136 return false 137 }, retrySeconds, time.Second, "Rename not detected by client") 138 } 139 140 // Change the content remotely on the server, and verify it gets propagated to 141 // to the client. 142 func TestDeltaContentChangeRemote(t *testing.T) { 143 t.Parallel() 144 require.NoError(t, ioutil.WriteFile( 145 filepath.Join(DeltaDir, "remote_content"), 146 []byte("the cake is a lie"), 147 0644, 148 )) 149 150 // change and upload it via the API 151 time.Sleep(time.Second * 10) 152 item, err := graph.GetItemPath("/onedriver_tests/delta/remote_content", auth) 153 inode := NewInodeDriveItem(item) 154 require.NoError(t, err) 155 newContent := []byte("because it has been changed remotely!") 156 inode.setContent(fs, newContent) 157 data := fs.content.Get(inode.ID()) 158 session, err := NewUploadSession(inode, &data) 159 require.NoError(t, err) 160 require.NoError(t, session.Upload(auth)) 161 162 time.Sleep(time.Second * 10) 163 body, _, _ := graph.GetItemContent(inode.ID(), auth) 164 if !bytes.Equal(body, newContent) { 165 t.Fatalf("Failed to upload test file. Remote content: \"%s\"", body) 166 } 167 168 var content []byte 169 assert.Eventuallyf(t, func() bool { 170 content, err = ioutil.ReadFile(filepath.Join(DeltaDir, "remote_content")) 171 require.NoError(t, err) 172 return bytes.Equal(content, newContent) 173 }, retrySeconds, time.Second, 174 "Failed to sync content to local machine. Got content: \"%s\". "+ 175 "Wanted: \"because it has been changed remotely!\". "+ 176 "Remote content: \"%s\".", 177 string(content), string(body), 178 ) 179 } 180 181 // Change the content both on the server and the client and verify that the 182 // client data is preserved. 183 func TestDeltaContentChangeBoth(t *testing.T) { 184 t.Parallel() 185 186 cache := NewFilesystem(auth, filepath.Join(testDBLoc, "test_delta_content_change_both")) 187 inode := NewInode("both_content_changed.txt", 0644|fuse.S_IFREG, nil) 188 cache.InsertPath("/both_content_changed.txt", nil, inode) 189 original := []byte("initial content") 190 inode.setContent(cache, original) 191 192 // write to, but do not close the file to simulate an in-use local file 193 local := []byte("local write content") 194 _, status := cache.Write( 195 context.Background().Done(), 196 &fuse.WriteIn{ 197 InHeader: fuse.InHeader{NodeId: inode.NodeID()}, 198 Offset: 0, 199 Size: uint32(len(local)), 200 }, 201 local, 202 ) 203 if status != fuse.OK { 204 t.Fatal("Write failed") 205 } 206 207 // apply a fake delta to the local item 208 fakeDelta := inode.DriveItem 209 now := time.Now().Add(time.Second * 10) 210 fakeDelta.ModTime = &now 211 fakeDelta.Size = uint64(len(original)) 212 fakeDelta.ETag = "sldfjlsdjflkdj" 213 fakeDelta.File.Hashes = graph.Hashes{ 214 QuickXorHash: graph.QuickXORHash(&original), 215 } 216 217 // should do nothing 218 require.NoError(t, cache.applyDelta(&fakeDelta)) 219 require.Equal(t, uint64(len(local)), inode.Size(), "Contents of open local file changed!") 220 221 // act as if the file is now flushed (these are the ops that would happen during 222 // a flush) 223 inode.DriveItem.File = &graph.File{} 224 fd, _ := fs.content.Open(inode.ID()) 225 inode.DriveItem.File.Hashes.QuickXorHash = graph.QuickXORHashStream(fd) 226 cache.content.Close(inode.DriveItem.ID) 227 inode.hasChanges = false 228 229 // should now change the file 230 require.NoError(t, cache.applyDelta(&fakeDelta)) 231 require.Equal(t, fakeDelta.Size, inode.Size(), 232 "Contents of local file was not changed after disabling local changes!") 233 } 234 235 // If we have local content in the local disk cache that doesn't match what the 236 // server has, Open() should pick this up and wipe it. Otherwise Open() could 237 // pick up an old version of a file from previous program startups and think 238 // it's current, which would erase the real, up-to-date server copy. 239 func TestDeltaBadContentInCache(t *testing.T) { 240 t.Parallel() 241 // write a file to the server and poll until it exists 242 require.NoError(t, ioutil.WriteFile( 243 filepath.Join(DeltaDir, "corrupted"), 244 []byte("correct contents"), 245 0644, 246 )) 247 var id string 248 require.Eventually(t, func() bool { 249 item, err := graph.GetItemPath("/onedriver_tests/delta/corrupted", auth) 250 if err == nil { 251 id = item.ID 252 return true 253 } 254 return false 255 }, retrySeconds, time.Second) 256 257 fs.content.Insert(id, []byte("wrong contents")) 258 contents, err := ioutil.ReadFile(filepath.Join(DeltaDir, "corrupted")) 259 require.NoError(t, err) 260 if bytes.HasPrefix(contents, []byte("wrong")) { 261 t.Fatalf("File contents were wrong! Got \"%s\", wanted \"correct contents\"", 262 string(contents)) 263 } 264 } 265 266 // Check that folders are deleted only when empty after syncing the complete set of 267 // changes. 268 func TestDeltaFolderDeletion(t *testing.T) { 269 t.Parallel() 270 require.NoError(t, os.MkdirAll(filepath.Join(DeltaDir, "nested/directory"), 0755)) 271 nested, err := graph.GetItemPath("/onedriver_tests/delta/nested", auth) 272 require.NoError(t, err) 273 require.NoError(t, graph.Remove(nested.ID, auth)) 274 275 // now poll and wait for deletion 276 assert.Eventually(t, func() bool { 277 inodes, _ := ioutil.ReadDir(DeltaDir) 278 for _, inode := range inodes { 279 if inode.Name() == "nested" { 280 return true 281 } 282 } 283 return false 284 }, retrySeconds, time.Second, "\"nested/\" directory was not deleted.") 285 } 286 287 // We should only perform a delta deletion of a folder if it was nonempty 288 func TestDeltaFolderDeletionNonEmpty(t *testing.T) { 289 t.Parallel() 290 cache := NewFilesystem(auth, filepath.Join(testDBLoc, "test_delta_folder_deletion_nonempty")) 291 dir := NewInode("folder", 0755|fuse.S_IFDIR, nil) 292 file := NewInode("file", 0644|fuse.S_IFREG, nil) 293 cache.InsertPath("/folder", nil, dir) 294 cache.InsertPath("/folder/file", nil, file) 295 296 delta := &graph.DriveItem{ 297 ID: dir.ID(), 298 Parent: &graph.DriveItemParent{ID: dir.ParentID()}, 299 Deleted: &graph.Deleted{State: "softdeleted"}, 300 Folder: &graph.Folder{}, 301 } 302 err := cache.applyDelta(delta) 303 require.NotNil(t, cache.GetID(delta.ID), "Folder should still be present") 304 require.Error(t, err, "A delta deletion of a non-empty folder was not an error") 305 306 cache.DeletePath("/folder/file") 307 cache.applyDelta(delta) 308 assert.Nil(t, cache.GetID(delta.ID), 309 "Still found folder after emptying it first (the correct way).") 310 } 311 312 // Some programs like LibreOffice and WPS Office will have a fit if the 313 // modification times on their lockfiles is updated after they are written. This 314 // test verifies that the delta thread does not modify modification times if the 315 // content is unchanged. 316 func TestDeltaNoModTimeUpdate(t *testing.T) { 317 t.Parallel() 318 fname := filepath.Join(DeltaDir, "mod_time_update.txt") 319 require.NoError(t, ioutil.WriteFile(fname, []byte("a pretend lockfile"), 0644)) 320 finfo, err := os.Stat(fname) 321 require.NoError(t, err) 322 mtimeOriginal := finfo.ModTime() 323 324 time.Sleep(15 * time.Second) 325 326 finfo, err = os.Stat(fname) 327 require.NoError(t, err) 328 mtimeNew := finfo.ModTime() 329 if !mtimeNew.Equal(mtimeOriginal) { 330 t.Fatalf( 331 "Modification time was updated even though the file did not change.\n"+ 332 "Old mtime: %d, New mtime: %d\n", mtimeOriginal.Unix(), mtimeNew.Unix(), 333 ) 334 } 335 } 336 337 // deltas can come back missing from the server 338 // https://github.com/jstaf/onedriver/issues/111 339 func TestDeltaMissingHash(t *testing.T) { 340 t.Parallel() 341 cache := NewFilesystem(auth, filepath.Join(testDBLoc, "test_delta_missing_hash")) 342 file := NewInode("file", 0644|fuse.S_IFREG, nil) 343 cache.InsertPath("/folder", nil, file) 344 345 time.Sleep(time.Second) 346 now := time.Now() 347 delta := &graph.DriveItem{ 348 ID: file.ID(), 349 Parent: &graph.DriveItemParent{ID: file.ParentID()}, 350 ModTime: &now, 351 Size: 12345, 352 } 353 cache.applyDelta(delta) 354 // if we survive to here without a segfault, test passed 355 }