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  }