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 }