github.com/jstaf/onedriver@v0.14.2-0.20240420231225-f07678f9e6ef/fs/fs_test.go (about)

     1  // A bunch of "black box" filesystem integration tests that test the
     2  // functionality of key syscalls and their implementation. If something fails
     3  // here, the filesystem is not functional.
     4  package fs
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"io/ioutil"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strings"
    14  	"syscall"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/jstaf/onedriver/fs/graph"
    19  	"github.com/stretchr/testify/assert"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  // Does Go's internal ReadDir function work? This is mostly here to compare against
    24  // the offline versions of this test.
    25  func TestReaddir(t *testing.T) {
    26  	t.Parallel()
    27  	files, err := ioutil.ReadDir("mount")
    28  	if err != nil {
    29  		t.Fatal(err)
    30  	}
    31  
    32  	for _, file := range files {
    33  		if file.Name() == "Documents" {
    34  			return
    35  		}
    36  	}
    37  	t.Fatal("Could not find \"Documents\" folder.")
    38  }
    39  
    40  // does ls work and can we find the Documents folder?
    41  func TestLs(t *testing.T) {
    42  	t.Parallel()
    43  	stdout, err := exec.Command("ls", "mount").Output()
    44  	require.NoError(t, err)
    45  	sout := string(stdout)
    46  	if !strings.Contains(sout, "Documents") {
    47  		t.Fatal("Could not find \"Documents\" folder.")
    48  	}
    49  }
    50  
    51  // can touch create an empty file?
    52  func TestTouchCreate(t *testing.T) {
    53  	t.Parallel()
    54  	fname := filepath.Join(TestDir, "empty")
    55  	syscall.Umask(022) // otherwise tests fail if default umask is 002
    56  	require.NoError(t, exec.Command("touch", fname).Run())
    57  	st, err := os.Stat(fname)
    58  	require.NoError(t, err)
    59  
    60  	require.Zero(t, st.Size(), "Size should be zero.")
    61  	if st.Mode() != 0644 {
    62  		t.Fatal("Mode of new file was not 644, got", Octal(uint32(st.Mode())))
    63  	}
    64  	require.False(t, st.IsDir(), "New file detected as directory.")
    65  }
    66  
    67  // does the touch command update modification time properly?
    68  func TestTouchUpdateTime(t *testing.T) {
    69  	t.Parallel()
    70  	fname := filepath.Join(TestDir, "modtime")
    71  	require.NoError(t, exec.Command("touch", fname).Run())
    72  	st1, _ := os.Stat(fname)
    73  
    74  	time.Sleep(2 * time.Second)
    75  
    76  	require.NoError(t, exec.Command("touch", fname).Run())
    77  	st2, _ := os.Stat(fname)
    78  
    79  	if st2.ModTime().Equal(st1.ModTime()) || st2.ModTime().Before(st1.ModTime()) {
    80  		t.Fatalf("File modification time was not updated by touch:\n"+
    81  			"Before: %d\nAfter: %d\n", st1.ModTime().Unix(), st2.ModTime().Unix())
    82  	}
    83  }
    84  
    85  // chmod should *just work*
    86  func TestChmod(t *testing.T) {
    87  	t.Parallel()
    88  	fname := filepath.Join(TestDir, "chmod_tester")
    89  	require.NoError(t, exec.Command("touch", fname).Run())
    90  	require.NoError(t, os.Chmod(fname, 0777))
    91  	st, _ := os.Stat(fname)
    92  	if st.Mode() != 0777 {
    93  		t.Fatalf("Mode of file was not 0777, got %o instead!", st.Mode())
    94  	}
    95  }
    96  
    97  // test that both mkdir and rmdir work, as well as the potentially failing
    98  // mkdir->rmdir->mkdir chain that fails if the cache hangs on to an old copy
    99  // after rmdir
   100  func TestMkdirRmdir(t *testing.T) {
   101  	t.Parallel()
   102  	fname := filepath.Join(TestDir, "folder1")
   103  	require.NoError(t, os.Mkdir(fname, 0755))
   104  	require.NoError(t, os.Remove(fname))
   105  	require.NoError(t, os.Mkdir(fname, 0755))
   106  }
   107  
   108  // We shouldn't be able to rmdir nonempty directories
   109  func TestRmdirNonempty(t *testing.T) {
   110  	t.Parallel()
   111  	dir := filepath.Join(TestDir, "nonempty")
   112  	require.NoError(t, os.Mkdir(dir, 0755))
   113  	require.NoError(t, os.Mkdir(filepath.Join(dir, "contents"), 0755))
   114  
   115  	require.Error(t, os.Remove(dir), "We somehow removed a nonempty directory!")
   116  
   117  	require.NoError(t, os.RemoveAll(dir),
   118  		"Could not remove a nonempty directory the correct way!")
   119  }
   120  
   121  // test that we can write to a file and read its contents back correctly
   122  func TestReadWrite(t *testing.T) {
   123  	t.Parallel()
   124  	fname := filepath.Join(TestDir, "write.txt")
   125  	content := "my hands are typing words\n"
   126  	require.NoError(t, ioutil.WriteFile(fname, []byte(content), 0644))
   127  	read, err := ioutil.ReadFile(fname)
   128  	require.NoError(t, err)
   129  	assert.Equal(t, content, string(read), "File content was not correct.")
   130  }
   131  
   132  // ld can crash the filesystem because it starts writing output at byte 64 in previously
   133  // empty file
   134  func TestWriteOffset(t *testing.T) {
   135  	t.Parallel()
   136  	fname := filepath.Join(TestDir, "main.c")
   137  	require.NoError(t, ioutil.WriteFile(fname,
   138  		[]byte(`#include <stdio.h>
   139  
   140  int main(int argc, char **argv) {
   141  	printf("ld writes files in a funny manner!");
   142  }`), 0644))
   143  	require.NoError(t, exec.Command("gcc", "-o", filepath.Join(TestDir, "main.o"), fname).Run())
   144  }
   145  
   146  // test that we can create a file and rename it
   147  // TODO this can fail if a server-side rename undoes the second local rename
   148  func TestRenameMove(t *testing.T) {
   149  	t.Parallel()
   150  	fname := filepath.Join(TestDir, "rename.txt")
   151  	dname := filepath.Join(TestDir, "new-destination-name.txt")
   152  	require.NoError(t, ioutil.WriteFile(fname, []byte("hopefully renames work\n"), 0644))
   153  	require.NoError(t, os.Rename(fname, dname))
   154  	st, err := os.Stat(dname)
   155  	require.NoError(t, err)
   156  	require.NotNil(t, st, "Renamed file does not exist.")
   157  
   158  	os.Mkdir(filepath.Join(TestDir, "dest"), 0755)
   159  	dname2 := filepath.Join(TestDir, "dest/even-newer-name.txt")
   160  	require.NoError(t, os.Rename(dname, dname2))
   161  	st, err = os.Stat(dname2)
   162  	require.NoError(t, err)
   163  	require.NotNil(t, st, "Renamed file does not exist.")
   164  }
   165  
   166  // test that copies work as expected
   167  func TestCopy(t *testing.T) {
   168  	t.Parallel()
   169  	fname := filepath.Join(TestDir, "copy-start.txt")
   170  	dname := filepath.Join(TestDir, "copy-end.txt")
   171  	content := "and copies too!\n"
   172  	require.NoError(t, ioutil.WriteFile(fname, []byte(content), 0644))
   173  	require.NoError(t, exec.Command("cp", fname, dname).Run())
   174  
   175  	read, err := ioutil.ReadFile(fname)
   176  	require.NoError(t, err)
   177  	assert.Equal(t, content, string(read), "File content was not correct.")
   178  }
   179  
   180  // do appends work correctly?
   181  func TestAppend(t *testing.T) {
   182  	t.Parallel()
   183  	fname := filepath.Join(TestDir, "append.txt")
   184  	for i := 0; i < 5; i++ {
   185  		file, _ := os.OpenFile(fname, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
   186  		file.WriteString("append\n")
   187  		file.Close()
   188  	}
   189  
   190  	file, err := os.Open(fname)
   191  	require.NoError(t, err)
   192  	defer file.Close()
   193  
   194  	scanner := bufio.NewScanner(file)
   195  	var counter int
   196  	for scanner.Scan() {
   197  		counter++
   198  		scanned := scanner.Text()
   199  		if scanned != "append" {
   200  			t.Fatalf("File text was wrong. Got \"%s\", wanted \"append\"\n", scanned)
   201  		}
   202  	}
   203  	if counter != 5 {
   204  		t.Fatalf("Got wrong number of lines (%d), expected 5\n", counter)
   205  	}
   206  }
   207  
   208  // identical to TestAppend, but truncates the file each time it is written to
   209  func TestTruncate(t *testing.T) {
   210  	t.Parallel()
   211  	fname := filepath.Join(TestDir, "truncate.txt")
   212  	for i := 0; i < 5; i++ {
   213  		file, _ := os.OpenFile(fname, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0644)
   214  		file.WriteString("append\n")
   215  		file.Close()
   216  	}
   217  
   218  	file, err := os.Open(fname)
   219  	require.NoError(t, err)
   220  	defer file.Close()
   221  
   222  	scanner := bufio.NewScanner(file)
   223  	var counter int
   224  	for scanner.Scan() {
   225  		counter++
   226  		assert.Equal(t, "append", scanner.Text(), "File text was wrong.")
   227  	}
   228  	if counter != 1 {
   229  		t.Fatalf("Got wrong number of lines (%d), expected 1\n", counter)
   230  	}
   231  }
   232  
   233  // can we seek to the middle of a file and do writes there correctly?
   234  func TestReadWriteMidfile(t *testing.T) {
   235  	t.Parallel()
   236  	content := `Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
   237  Phasellus viverra dui vel velit eleifend, vel auctor nulla scelerisque.
   238  Mauris volutpat a justo vel suscipit. Suspendisse diam lorem, imperdiet eget
   239  fermentum ut, sodales a nunc. Phasellus eget mattis purus. Aenean vitae justo
   240  condimentum, rutrum libero non, commodo ex. Nullam mi metus, accumsan sit
   241  amet varius non, volutpat eget mi. Fusce sollicitudin arcu eget ipsum
   242  gravida, ut blandit turpis facilisis. Quisque vel rhoncus nulla, ultrices
   243  tempor turpis. Nullam urna leo, dapibus eu velit eu, venenatis aliquet
   244  tortor. In tempus lacinia est, nec gravida ipsum viverra sed. In vel felis
   245  vitae odio pulvinar egestas. Sed ullamcorper, nulla non molestie dictum,
   246  massa lectus mattis dolor, in volutpat nulla lectus id neque.`
   247  	fname := filepath.Join(TestDir, "midfile.txt")
   248  	require.NoError(t, ioutil.WriteFile(fname, []byte(content), 0644))
   249  
   250  	file, _ := os.OpenFile(fname, os.O_RDWR, 0644)
   251  	defer file.Close()
   252  	match := "my hands are typing words. aaaaaaa"
   253  
   254  	n, err := file.WriteAt([]byte(match), 123)
   255  	require.NoError(t, err)
   256  	require.Equal(t, len(match), n, "Wrong number of bytes written.")
   257  
   258  	result := make([]byte, len(match))
   259  	n, err = file.ReadAt(result, 123)
   260  	require.NoError(t, err)
   261  	require.Equal(t, len(match), n, "Wrong number of bytes read.")
   262  
   263  	require.Equal(t, match, string(result), "Content did not match expected output.")
   264  }
   265  
   266  // Statfs should succeed
   267  func TestStatFs(t *testing.T) {
   268  	t.Parallel()
   269  	var st syscall.Statfs_t
   270  	err := syscall.Statfs(TestDir, &st)
   271  	require.NoError(t, err)
   272  	require.NotZero(t, st.Blocks, "StatFs failed, got 0 blocks!")
   273  }
   274  
   275  // does unlink work? (because apparently we weren't testing that before...)
   276  func TestUnlink(t *testing.T) {
   277  	t.Parallel()
   278  	fname := filepath.Join(TestDir, "unlink_tester")
   279  	require.NoError(t, exec.Command("touch", fname).Run())
   280  	require.NoError(t, os.Remove(fname))
   281  	stdout, _ := exec.Command("ls", "mount").Output()
   282  	if strings.Contains(string(stdout), "unlink_tester") {
   283  		t.Fatalf("Deleting %s did not work.", fname)
   284  	}
   285  }
   286  
   287  // OneDrive is case-insensitive due to limitations imposed by Windows NTFS
   288  // filesystem. Make sure we prevent users of normal systems from running into
   289  // issues with OneDrive's case-insensitivity.
   290  func TestNTFSIsABadFilesystem(t *testing.T) {
   291  	t.Parallel()
   292  	require.NoError(t, ioutil.WriteFile(filepath.Join(TestDir, "case-sensitive.txt"),
   293  		[]byte("NTFS is bad"), 0644))
   294  	require.NoError(t, ioutil.WriteFile(filepath.Join(TestDir, "CASE-SENSITIVE.txt"),
   295  		[]byte("yep"), 0644))
   296  
   297  	content, err := ioutil.ReadFile(filepath.Join(TestDir, "Case-Sensitive.TXT"))
   298  	require.NoError(t, err)
   299  	require.Equal(t, "yep", string(content), "Did not find expected output.")
   300  }
   301  
   302  // same as last test, but with exclusive create() calls.
   303  func TestNTFSIsABadFilesystem2(t *testing.T) {
   304  	t.Parallel()
   305  	file, err := os.OpenFile(filepath.Join(TestDir, "case-sensitive2.txt"), os.O_CREATE|os.O_EXCL, 0644)
   306  	file.Close()
   307  	require.NoError(t, err)
   308  
   309  	file, err = os.OpenFile(filepath.Join(TestDir, "CASE-SENSITIVE2.txt"), os.O_CREATE|os.O_EXCL, 0644)
   310  	file.Close()
   311  	require.Error(t, err,
   312  		"We should be throwing an error, since OneDrive is case-insensitive.")
   313  }
   314  
   315  // Ensure that case-sensitivity collisions due to renames are handled properly
   316  // (allow rename/overwrite for exact matches, deny when case-sensitivity would
   317  // normally allow success)
   318  func TestNTFSIsABadFilesystem3(t *testing.T) {
   319  	t.Parallel()
   320  	fname := filepath.Join(TestDir, "original_NAME.txt")
   321  	ioutil.WriteFile(fname, []byte("original"), 0644)
   322  
   323  	// should work
   324  	secondName := filepath.Join(TestDir, "new_name.txt")
   325  	require.NoError(t, ioutil.WriteFile(secondName, []byte("new"), 0644))
   326  	require.NoError(t, os.Rename(secondName, fname))
   327  	contents, err := ioutil.ReadFile(fname)
   328  	require.NoError(t, err)
   329  	require.Equal(t, "new", string(contents), "Contents did not match expected output.")
   330  
   331  	// should fail
   332  	thirdName := filepath.Join(TestDir, "new_name2.txt")
   333  	require.NoError(t, ioutil.WriteFile(thirdName, []byte("this rename should work"), 0644))
   334  	err = os.Rename(thirdName, filepath.Join(TestDir, "original_name.txt"))
   335  	require.NoError(t, err, "Rename failed.")
   336  
   337  	_, err = os.Stat(fname)
   338  	require.NoErrorf(t, err, "\"%s\" does not exist after the rename.", fname)
   339  }
   340  
   341  // This test is insurance to prevent tests (and the fs) from accidentally not
   342  // storing case for filenames at all
   343  func TestChildrenAreCasedProperly(t *testing.T) {
   344  	t.Parallel()
   345  	require.NoError(t, ioutil.WriteFile(
   346  		filepath.Join(TestDir, "CASE-check.txt"), []byte("yep"), 0644))
   347  	stdout, err := exec.Command("ls", TestDir).Output()
   348  	if err != nil {
   349  		t.Fatalf("%s: %s", err, stdout)
   350  	}
   351  	if !strings.Contains(string(stdout), "CASE-check.txt") {
   352  		t.Fatalf("Upper case filenames were not honored, "+
   353  			"expected \"CASE-check.txt\" in output, got %s\n", string(stdout))
   354  	}
   355  }
   356  
   357  // Test that when running "echo some text > file.txt" that file.txt actually
   358  // becomes populated
   359  func TestEchoWritesToFile(t *testing.T) {
   360  	t.Parallel()
   361  	fname := filepath.Join(TestDir, "bagels")
   362  	out, err := exec.Command("bash", "-c", "echo bagels > "+fname).CombinedOutput()
   363  	require.NoError(t, err, out)
   364  
   365  	content, err := ioutil.ReadFile(fname)
   366  	require.NoError(t, err)
   367  	if !bytes.Contains(content, []byte("bagels")) {
   368  		t.Fatalf("Populating a file via 'echo' failed. Got: \"%s\", wanted \"bagels\"\n", content)
   369  	}
   370  }
   371  
   372  // Test that if we stat a file, we get some correct information back
   373  func TestStat(t *testing.T) {
   374  	t.Parallel()
   375  	stat, err := os.Stat("mount/Documents")
   376  	require.NoError(t, err)
   377  	require.Equal(t, "Documents", stat.Name(), "Name was not \"Documents\".")
   378  
   379  	if stat.ModTime().Year() < 1971 {
   380  		t.Fatal("Modification time of /Documents wrong, got: " + stat.ModTime().String())
   381  	}
   382  
   383  	if !stat.IsDir() {
   384  		t.Fatalf("Mode of /Documents wrong, not detected as directory, got: %s", stat.Mode())
   385  	}
   386  }
   387  
   388  // Question marks appear in `ls -l`s output if an item is populated via readdir,
   389  // but subsequently not found by lookup. Also is a nice catch-all for fs
   390  // metadata corruption, as `ls` will exit with 1 if something bad happens.
   391  func TestNoQuestionMarks(t *testing.T) {
   392  	t.Parallel()
   393  	out, err := exec.Command("ls", "-l", "mount/").CombinedOutput()
   394  	if strings.Contains(string(out), "??????????") || err != nil {
   395  		t.Log("A Lookup() failed on an inode found by Readdir()")
   396  		t.Log(string(out))
   397  		t.FailNow()
   398  	}
   399  }
   400  
   401  // Trashing items through nautilus or other Linux file managers is done via
   402  // "gio trash". Make an item then trash it to verify that this works.
   403  func TestGIOTrash(t *testing.T) {
   404  	t.Parallel()
   405  	fname := filepath.Join(TestDir, "trash_me.txt")
   406  	require.NoError(t, ioutil.WriteFile(fname, []byte("i should be trashed"), 0644))
   407  
   408  	out, err := exec.Command("gio", "trash", fname).CombinedOutput()
   409  	if err != nil {
   410  		t.Log(string(out))
   411  		t.Log(err)
   412  		if st, err2 := os.Stat(fname); err2 == nil {
   413  			if !st.IsDir() && strings.Contains(string(out), "Is a directory") {
   414  				t.Skip("This is a GIO bug (it complains about test file being " +
   415  					"a directory despite correct metadata from onedriver), skipping.")
   416  			}
   417  			t.Fatal(fname, "still exists after deletion!")
   418  		}
   419  	}
   420  	if strings.Contains(string(out), "Unable to find or create trash directory") {
   421  		t.Fatal(string(out))
   422  	}
   423  }
   424  
   425  // Test that we are able to work around onedrive paging limits when
   426  // listing a folder's children.
   427  func TestListChildrenPaging(t *testing.T) {
   428  	t.Parallel()
   429  	// files have been prepopulated during test setup to avoid being picked up by
   430  	// the delta thread
   431  	items, err := graph.GetItemChildrenPath("/onedriver_tests/paging", auth)
   432  	require.NoError(t, err)
   433  	files, err := ioutil.ReadDir(filepath.Join(TestDir, "paging"))
   434  	require.NoError(t, err)
   435  	if len(files) < 201 {
   436  		if len(items) < 201 {
   437  			t.Logf("Skipping test, number of paging files from the API were also less than 201.\nAPI: %d\nFS: %d\n",
   438  				len(items), len(files),
   439  			)
   440  			t.SkipNow()
   441  		}
   442  		t.Fatalf("Paging limit failed. Got %d files, wanted at least 201.\n", len(files))
   443  	}
   444  }
   445  
   446  // Libreoffice writes to files in a funny manner and it can result in a 0 byte file
   447  // being uploaded (can check syscalls via "inotifywait -m -r .").
   448  func TestLibreOfficeSavePattern(t *testing.T) {
   449  	t.Parallel()
   450  	content := []byte("This will break things.")
   451  	fname := filepath.Join(TestDir, "libreoffice.txt")
   452  	require.NoError(t, ioutil.WriteFile(fname, content, 0644))
   453  
   454  	out, err := exec.Command(
   455  		"libreoffice",
   456  		"--headless",
   457  		"--convert-to", "docx",
   458  		"--outdir", TestDir,
   459  		fname,
   460  	).CombinedOutput()
   461  	require.NoError(t, err, out)
   462  	// libreoffice document conversion can fail with an exit code of 0,
   463  	// so we need to actually check the command output
   464  	require.NotContains(t, string(out), "Error:")
   465  
   466  	assert.Eventually(t, func() bool {
   467  		item, err := graph.GetItemPath("/onedriver_tests/libreoffice.docx", auth)
   468  		if err == nil && item != nil {
   469  			if item.Size == 0 {
   470  				t.Fatal("Item size was 0!")
   471  			}
   472  			return true
   473  		}
   474  		return false
   475  	}, retrySeconds, 3*time.Second,
   476  		"Could not find /onedriver_tests/libreoffice.docx post-upload!",
   477  	)
   478  }
   479  
   480  // TestDisallowedFilenames verifies that we can't create any of the disallowed filenames
   481  // https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa
   482  func TestDisallowedFilenames(t *testing.T) {
   483  	t.Parallel()
   484  	contents := []byte("this should not work")
   485  	assert.Error(t, os.WriteFile(filepath.Join(TestDir, "disallowed: filename.txt"), contents, 0644))
   486  	assert.Error(t, os.WriteFile(filepath.Join(TestDir, "disallowed_vti_text.txt"), contents, 0644))
   487  	assert.Error(t, os.WriteFile(filepath.Join(TestDir, "disallowed_<_text.txt"), contents, 0644))
   488  	assert.Error(t, os.WriteFile(filepath.Join(TestDir, "COM0"), contents, 0644))
   489  	assert.Error(t, os.Mkdir(filepath.Join(TestDir, "disallowed:folder"), 0755))
   490  	assert.Error(t, os.Mkdir(filepath.Join(TestDir, "disallowed_vti_folder"), 0755))
   491  	assert.Error(t, os.Mkdir(filepath.Join(TestDir, "disallowed>folder"), 0755))
   492  	assert.Error(t, os.Mkdir(filepath.Join(TestDir, "desktop.ini"), 0755))
   493  
   494  	require.NoError(t, os.Mkdir(filepath.Join(TestDir, "valid-directory"), 0755))
   495  	assert.Error(t, os.Rename(
   496  		filepath.Join(TestDir, "valid-directory"),
   497  		filepath.Join(TestDir, "invalid_vti_directory"),
   498  	))
   499  }