github.com/artpar/rclone@v1.67.3/cmd/gitannex/gitannex_test.go (about) 1 package gitannex 2 3 import ( 4 "bufio" 5 "crypto/sha256" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "strings" 11 "sync" 12 "testing" 13 14 // Without this import, the local filesystem backend would be unavailable. 15 // It looks unused, but the act of importing it runs its `init()` function. 16 _ "github.com/artpar/rclone/backend/local" 17 18 "github.com/artpar/rclone/fs" 19 "github.com/artpar/rclone/fs/cache" 20 "github.com/artpar/rclone/fs/config" 21 "github.com/artpar/rclone/fs/config/configfile" 22 "github.com/artpar/rclone/fstest/mockfs" 23 24 "github.com/stretchr/testify/assert" 25 "github.com/stretchr/testify/require" 26 ) 27 28 func TestFixArgsForSymlinkIdentity(t *testing.T) { 29 for _, argList := range [][]string{ 30 []string{}, 31 []string{"foo"}, 32 []string{"foo", "bar"}, 33 []string{"foo", "bar", "baz"}, 34 } { 35 assert.Equal(t, maybeTransformArgs(argList), argList) 36 } 37 } 38 39 func TestFixArgsForSymlinkCorrectName(t *testing.T) { 40 assert.Equal(t, 41 maybeTransformArgs([]string{"git-annex-remote-rclone-builtin"}), 42 []string{"git-annex-remote-rclone-builtin", "gitannex"}) 43 assert.Equal(t, 44 maybeTransformArgs([]string{"/path/to/git-annex-remote-rclone-builtin"}), 45 []string{"/path/to/git-annex-remote-rclone-builtin", "gitannex"}) 46 } 47 48 type messageParserTestCase struct { 49 label string 50 testFunc func(*testing.T) 51 } 52 53 var messageParserTestCases = []messageParserTestCase{ 54 { 55 "OneParam", 56 func(t *testing.T) { 57 m := messageParser{"foo\n"} 58 59 param, err := m.nextSpaceDelimitedParameter() 60 assert.NoError(t, err) 61 assert.Equal(t, param, "foo") 62 63 param, err = m.nextSpaceDelimitedParameter() 64 assert.Error(t, err) 65 assert.Equal(t, param, "") 66 67 param, err = m.finalParameter() 68 assert.Error(t, err) 69 assert.Equal(t, param, "") 70 71 param, err = m.finalParameter() 72 assert.Error(t, err) 73 assert.Equal(t, param, "") 74 75 param, err = m.nextSpaceDelimitedParameter() 76 assert.Error(t, err) 77 assert.Equal(t, param, "") 78 79 }, 80 }, 81 { 82 "TwoParams", 83 func(t *testing.T) { 84 m := messageParser{"foo bar\n"} 85 86 param, err := m.nextSpaceDelimitedParameter() 87 assert.NoError(t, err) 88 assert.Equal(t, param, "foo") 89 90 param, err = m.nextSpaceDelimitedParameter() 91 assert.NoError(t, err) 92 assert.Equal(t, param, "bar") 93 94 param, err = m.nextSpaceDelimitedParameter() 95 assert.Error(t, err) 96 assert.Equal(t, param, "") 97 98 param, err = m.finalParameter() 99 assert.Error(t, err) 100 assert.Equal(t, param, "") 101 }, 102 }, 103 { 104 "TwoParamsNoTrailingNewline", 105 106 func(t *testing.T) { 107 m := messageParser{"foo bar"} 108 109 param, err := m.nextSpaceDelimitedParameter() 110 assert.NoError(t, err) 111 assert.Equal(t, param, "foo") 112 113 param, err = m.nextSpaceDelimitedParameter() 114 assert.NoError(t, err) 115 assert.Equal(t, param, "bar") 116 117 param, err = m.nextSpaceDelimitedParameter() 118 assert.Error(t, err) 119 assert.Equal(t, param, "") 120 121 param, err = m.finalParameter() 122 assert.Error(t, err) 123 assert.Equal(t, param, "") 124 }, 125 }, 126 { 127 "ThreeParamsWhereFinalParamContainsSpaces", 128 func(t *testing.T) { 129 m := messageParser{"firstparam secondparam final param with spaces"} 130 131 param, err := m.nextSpaceDelimitedParameter() 132 assert.NoError(t, err) 133 assert.Equal(t, param, "firstparam") 134 135 param, err = m.nextSpaceDelimitedParameter() 136 assert.NoError(t, err) 137 assert.Equal(t, param, "secondparam") 138 139 param, err = m.finalParameter() 140 assert.NoError(t, err) 141 assert.Equal(t, param, "final param with spaces") 142 }, 143 }, 144 { 145 "OneLongFinalParameter", 146 func(t *testing.T) { 147 for _, lineEnding := range []string{"", "\n", "\r", "\r\n", "\n\r"} { 148 lineEnding := lineEnding 149 testName := fmt.Sprintf("lineEnding%x", lineEnding) 150 151 t.Run(testName, func(t *testing.T) { 152 m := messageParser{"one long final parameter" + lineEnding} 153 154 param, err := m.finalParameter() 155 assert.NoError(t, err) 156 assert.Equal(t, param, "one long final parameter") 157 158 param, err = m.finalParameter() 159 assert.Error(t, err) 160 assert.Equal(t, param, "") 161 }) 162 163 } 164 }, 165 }, 166 { 167 "MultipleSpaces", 168 func(t *testing.T) { 169 m := messageParser{"foo bar\n\r"} 170 171 param, err := m.nextSpaceDelimitedParameter() 172 assert.NoError(t, err) 173 assert.Equal(t, param, "foo") 174 175 param, err = m.nextSpaceDelimitedParameter() 176 assert.Error(t, err, "blah") 177 assert.Equal(t, param, "") 178 }, 179 }, 180 { 181 "StartsWithSpace", 182 func(t *testing.T) { 183 m := messageParser{" foo"} 184 185 param, err := m.nextSpaceDelimitedParameter() 186 assert.Error(t, err, "blah") 187 assert.Equal(t, param, "") 188 }, 189 }, 190 } 191 192 func TestMessageParser(t *testing.T) { 193 for _, testCase := range messageParserTestCases { 194 testCase := testCase 195 t.Run(testCase.label, func(t *testing.T) { 196 t.Parallel() 197 testCase.testFunc(t) 198 }) 199 } 200 } 201 202 type testState struct { 203 t *testing.T 204 server *server 205 mockStdinW *io.PipeWriter 206 mockStdoutReader *bufio.Reader 207 208 localFsDir string 209 configPath string 210 remoteName string 211 } 212 213 func makeTestState(t *testing.T) testState { 214 stdinR, stdinW := io.Pipe() 215 stdoutR, stdoutW := io.Pipe() 216 217 return testState{ 218 t: t, 219 server: &server{ 220 reader: bufio.NewReader(stdinR), 221 writer: stdoutW, 222 }, 223 mockStdinW: stdinW, 224 mockStdoutReader: bufio.NewReader(stdoutR), 225 } 226 } 227 228 func (h *testState) requireReadLineExact(line string) { 229 receivedLine, err := h.mockStdoutReader.ReadString('\n') 230 require.NoError(h.t, err) 231 require.Equal(h.t, line+"\n", receivedLine) 232 } 233 234 func (h *testState) requireWriteLine(line string) { 235 _, err := h.mockStdinW.Write([]byte(line + "\n")) 236 require.NoError(h.t, err) 237 } 238 239 // Preconfigure the handle. This enables the calling test to skip the PREPARE 240 // handshake. 241 func (h *testState) preconfigureServer() { 242 h.server.configPrefix = h.localFsDir 243 h.server.configRcloneRemoteName = h.remoteName 244 h.server.configsDone = true 245 } 246 247 // getUniqueRemoteName returns a valid remote name derived from the given test's 248 // name. This is necessary because when a test registers a second remote with 249 // the same name, the original remote appears to take precedence. This function 250 // is injective, so each test gets a unique remote name. Returned strings 251 // contain no spaces. 252 func getUniqueRemoteName(t *testing.T) string { 253 // Using sha256 as a hack to ensure injectivity without adding a global 254 // variable. 255 return fmt.Sprintf("remote-%x", sha256.Sum256([]byte(t.Name()))) 256 } 257 258 type testCase struct { 259 label string 260 testProtocolFunc func(*testing.T, *testState) 261 expectedError string 262 } 263 264 // These test cases run against the "local" backend. 265 var localBackendTestCases = []testCase{ 266 { 267 label: "HandlesInit", 268 testProtocolFunc: func(t *testing.T, h *testState) { 269 h.preconfigureServer() 270 271 h.requireReadLineExact("VERSION 1") 272 h.requireWriteLine("INITREMOTE") 273 h.requireReadLineExact("INITREMOTE-SUCCESS") 274 275 require.NoError(t, h.mockStdinW.Close()) 276 }, 277 }, 278 { 279 label: "HandlesPrepare", 280 testProtocolFunc: func(t *testing.T, h *testState) { 281 h.requireReadLineExact("VERSION 1") 282 h.requireWriteLine("EXTENSIONS INFO") // Advertise that we support the INFO extension 283 h.requireReadLineExact("EXTENSIONS") 284 285 if !h.server.extensionInfo { 286 t.Errorf("expected INFO extension to be enabled") 287 return 288 } 289 290 h.requireWriteLine("PREPARE") 291 h.requireReadLineExact("GETCONFIG rcloneremotename") 292 h.requireWriteLine("VALUE " + h.remoteName) 293 h.requireReadLineExact("GETCONFIG rcloneprefix") 294 h.requireWriteLine("VALUE " + h.localFsDir) 295 h.requireReadLineExact("PREPARE-SUCCESS") 296 297 require.Equal(t, h.server.configRcloneRemoteName, h.remoteName) 298 require.Equal(t, h.server.configPrefix, h.localFsDir) 299 require.True(t, h.server.configsDone) 300 301 require.NoError(t, h.mockStdinW.Close()) 302 }, 303 }, 304 { 305 label: "HandlesPrepareAndDoesNotTrimWhitespaceFromValue", 306 testProtocolFunc: func(t *testing.T, h *testState) { 307 h.requireReadLineExact("VERSION 1") 308 h.requireWriteLine("EXTENSIONS INFO") // Advertise that we support the INFO extension 309 h.requireReadLineExact("EXTENSIONS") 310 311 if !h.server.extensionInfo { 312 t.Errorf("expected INFO extension to be enabled") 313 return 314 } 315 316 h.requireWriteLine("PREPARE") 317 h.requireReadLineExact("GETCONFIG rcloneremotename") 318 319 remoteNameWithSpaces := fmt.Sprintf(" %s ", h.remoteName) 320 localFsDirWithSpaces := fmt.Sprintf(" %s\t", h.localFsDir) 321 322 h.requireWriteLine(fmt.Sprintf("VALUE %s", remoteNameWithSpaces)) 323 h.requireReadLineExact("GETCONFIG rcloneprefix") 324 325 h.requireWriteLine(fmt.Sprintf("VALUE %s", localFsDirWithSpaces)) 326 h.requireReadLineExact("PREPARE-SUCCESS") 327 328 require.Equal(t, h.server.configRcloneRemoteName, remoteNameWithSpaces) 329 require.Equal(t, h.server.configPrefix, localFsDirWithSpaces) 330 require.True(t, h.server.configsDone) 331 332 require.NoError(t, h.mockStdinW.Close()) 333 }, 334 }, 335 { 336 label: "HandlesEarlyError", 337 testProtocolFunc: func(t *testing.T, h *testState) { 338 h.preconfigureServer() 339 340 h.requireReadLineExact("VERSION 1") 341 h.requireWriteLine("ERROR foo") 342 343 require.NoError(t, h.mockStdinW.Close()) 344 }, 345 expectedError: "received error message from git-annex: foo", 346 }, 347 // Test what happens when the git-annex client sends "GETCONFIG", but 348 // doesn't understand git-annex's response. 349 { 350 label: "ConfigFail", 351 testProtocolFunc: func(t *testing.T, h *testState) { 352 h.requireReadLineExact("VERSION 1") 353 h.requireWriteLine("EXTENSIONS INFO") // Advertise that we support the INFO extension 354 h.requireReadLineExact("EXTENSIONS") 355 require.True(t, h.server.extensionInfo) 356 357 h.requireWriteLine("PREPARE") 358 h.requireReadLineExact("GETCONFIG rcloneremotename") 359 h.requireWriteLine("ERROR ineffable error") 360 h.requireReadLineExact("PREPARE-FAILURE Error getting configs") 361 362 require.NoError(t, h.mockStdinW.Close()) 363 }, 364 expectedError: "failed to parse config value: ERROR ineffable error", 365 }, 366 { 367 label: "TransferStoreEmptyPath", 368 testProtocolFunc: func(t *testing.T, h *testState) { 369 h.preconfigureServer() 370 371 h.requireReadLineExact("VERSION 1") 372 h.requireWriteLine("INITREMOTE") 373 h.requireReadLineExact("INITREMOTE-SUCCESS") 374 375 // Note the whitespace following the key. 376 h.requireWriteLine("TRANSFER STORE Key ") 377 h.requireReadLineExact("TRANSFER-FAILURE failed to parse file") 378 379 require.NoError(t, h.mockStdinW.Close()) 380 }, 381 expectedError: "malformed arguments for TRANSFER: nothing remains to parse", 382 }, 383 // Repeated EXTENSIONS messages add to each other rather than overriding 384 // prior advertised extensions. This behavior is not mandated by the 385 // protocol design. 386 { 387 label: "ExtensionsCompound", 388 testProtocolFunc: func(t *testing.T, h *testState) { 389 h.preconfigureServer() 390 391 h.requireReadLineExact("VERSION 1") 392 h.requireWriteLine("INITREMOTE") 393 h.requireReadLineExact("INITREMOTE-SUCCESS") 394 395 h.requireWriteLine("EXTENSIONS") 396 h.requireReadLineExact("EXTENSIONS") 397 require.False(t, h.server.extensionInfo) 398 require.False(t, h.server.extensionAsync) 399 require.False(t, h.server.extensionGetGitRemoteName) 400 require.False(t, h.server.extensionUnavailableResponse) 401 402 h.requireWriteLine("EXTENSIONS INFO") 403 h.requireReadLineExact("EXTENSIONS") 404 require.True(t, h.server.extensionInfo) 405 require.False(t, h.server.extensionAsync) 406 require.False(t, h.server.extensionGetGitRemoteName) 407 require.False(t, h.server.extensionUnavailableResponse) 408 409 h.requireWriteLine("EXTENSIONS ASYNC") 410 h.requireReadLineExact("EXTENSIONS") 411 require.True(t, h.server.extensionInfo) 412 require.True(t, h.server.extensionAsync) 413 require.False(t, h.server.extensionGetGitRemoteName) 414 require.False(t, h.server.extensionUnavailableResponse) 415 416 h.requireWriteLine("EXTENSIONS GETGITREMOTENAME") 417 h.requireReadLineExact("EXTENSIONS") 418 require.True(t, h.server.extensionInfo) 419 require.True(t, h.server.extensionAsync) 420 require.True(t, h.server.extensionGetGitRemoteName) 421 require.False(t, h.server.extensionUnavailableResponse) 422 423 h.requireWriteLine("EXTENSIONS UNAVAILABLERESPONSE") 424 h.requireReadLineExact("EXTENSIONS") 425 require.True(t, h.server.extensionInfo) 426 require.True(t, h.server.extensionAsync) 427 require.True(t, h.server.extensionGetGitRemoteName) 428 require.True(t, h.server.extensionUnavailableResponse) 429 430 require.NoError(t, h.mockStdinW.Close()) 431 }, 432 }, 433 { 434 label: "ExtensionsIdempotent", 435 testProtocolFunc: func(t *testing.T, h *testState) { 436 h.preconfigureServer() 437 438 h.requireReadLineExact("VERSION 1") 439 h.requireWriteLine("INITREMOTE") 440 h.requireReadLineExact("INITREMOTE-SUCCESS") 441 442 h.requireWriteLine("EXTENSIONS") 443 h.requireReadLineExact("EXTENSIONS") 444 require.False(t, h.server.extensionInfo) 445 require.False(t, h.server.extensionAsync) 446 require.False(t, h.server.extensionGetGitRemoteName) 447 require.False(t, h.server.extensionUnavailableResponse) 448 449 h.requireWriteLine("EXTENSIONS") 450 h.requireReadLineExact("EXTENSIONS") 451 require.False(t, h.server.extensionInfo) 452 require.False(t, h.server.extensionAsync) 453 require.False(t, h.server.extensionGetGitRemoteName) 454 require.False(t, h.server.extensionUnavailableResponse) 455 456 h.requireWriteLine("EXTENSIONS INFO") 457 h.requireReadLineExact("EXTENSIONS") 458 require.True(t, h.server.extensionInfo) 459 require.False(t, h.server.extensionAsync) 460 require.False(t, h.server.extensionGetGitRemoteName) 461 require.False(t, h.server.extensionUnavailableResponse) 462 463 h.requireWriteLine("EXTENSIONS INFO") 464 h.requireReadLineExact("EXTENSIONS") 465 require.True(t, h.server.extensionInfo) 466 require.False(t, h.server.extensionAsync) 467 require.False(t, h.server.extensionGetGitRemoteName) 468 require.False(t, h.server.extensionUnavailableResponse) 469 470 h.requireWriteLine("EXTENSIONS ASYNC ASYNC") 471 h.requireReadLineExact("EXTENSIONS") 472 require.True(t, h.server.extensionInfo) 473 require.True(t, h.server.extensionAsync) 474 require.False(t, h.server.extensionGetGitRemoteName) 475 require.False(t, h.server.extensionUnavailableResponse) 476 477 require.NoError(t, h.mockStdinW.Close()) 478 }, 479 }, 480 { 481 label: "ExtensionsSupportsMultiple", 482 testProtocolFunc: func(t *testing.T, h *testState) { 483 h.preconfigureServer() 484 485 h.requireReadLineExact("VERSION 1") 486 h.requireWriteLine("INITREMOTE") 487 h.requireReadLineExact("INITREMOTE-SUCCESS") 488 489 h.requireWriteLine("EXTENSIONS") 490 h.requireReadLineExact("EXTENSIONS") 491 require.False(t, h.server.extensionInfo) 492 require.False(t, h.server.extensionAsync) 493 require.False(t, h.server.extensionGetGitRemoteName) 494 require.False(t, h.server.extensionUnavailableResponse) 495 496 h.requireWriteLine("EXTENSIONS INFO ASYNC") 497 h.requireReadLineExact("EXTENSIONS") 498 require.True(t, h.server.extensionInfo) 499 require.True(t, h.server.extensionAsync) 500 require.False(t, h.server.extensionGetGitRemoteName) 501 require.False(t, h.server.extensionUnavailableResponse) 502 503 require.NoError(t, h.mockStdinW.Close()) 504 }, 505 }, 506 { 507 label: "TransferStoreAbsolute", 508 testProtocolFunc: func(t *testing.T, h *testState) { 509 h.preconfigureServer() 510 511 h.requireReadLineExact("VERSION 1") 512 h.requireWriteLine("INITREMOTE") 513 h.requireReadLineExact("INITREMOTE-SUCCESS") 514 515 // Create temp file for transfer with an absolute path. 516 fileToTransfer := filepath.Join(t.TempDir(), "file.txt") 517 require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600)) 518 require.FileExists(t, fileToTransfer) 519 require.True(t, filepath.IsAbs(fileToTransfer)) 520 521 // Specify an absolute path to transfer. 522 h.requireWriteLine("TRANSFER STORE KeyAbsolute " + fileToTransfer) 523 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyAbsolute") 524 require.FileExists(t, filepath.Join(h.localFsDir, "KeyAbsolute")) 525 526 // Transfer the same absolute path a second time, but with a different key. 527 h.requireWriteLine("TRANSFER STORE KeyAbsolute2 " + fileToTransfer) 528 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyAbsolute2") 529 require.FileExists(t, filepath.Join(h.localFsDir, "KeyAbsolute2")) 530 531 h.requireWriteLine("CHECKPRESENT KeyAbsolute2") 532 h.requireReadLineExact("CHECKPRESENT-SUCCESS KeyAbsolute2") 533 534 h.requireWriteLine("CHECKPRESENT KeyThatDoesNotExist") 535 h.requireReadLineExact("CHECKPRESENT-FAILURE KeyThatDoesNotExist") 536 537 require.NoError(t, h.mockStdinW.Close()) 538 }, 539 }, 540 // Test that the TRANSFER command understands simple relative paths 541 // consisting only of a file name. 542 { 543 label: "TransferStoreRelative", 544 testProtocolFunc: func(t *testing.T, h *testState) { 545 h.preconfigureServer() 546 547 // Save the current working directory so we can restore it when this 548 // test ends. 549 cwd, err := os.Getwd() 550 require.NoError(t, err) 551 552 require.NoError(t, os.Chdir(t.TempDir())) 553 t.Cleanup(func() { require.NoError(t, os.Chdir(cwd)) }) 554 555 h.requireReadLineExact("VERSION 1") 556 h.requireWriteLine("INITREMOTE") 557 h.requireReadLineExact("INITREMOTE-SUCCESS") 558 559 // Create temp file for transfer with a relative path. 560 fileToTransfer := "file.txt" 561 require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600)) 562 require.FileExists(t, fileToTransfer) 563 require.False(t, filepath.IsAbs(fileToTransfer)) 564 565 // Specify a relative path to transfer. 566 h.requireWriteLine("TRANSFER STORE KeyRelative " + fileToTransfer) 567 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyRelative") 568 require.FileExists(t, filepath.Join(h.localFsDir, "KeyRelative")) 569 570 h.requireWriteLine("CHECKPRESENT KeyRelative") 571 h.requireReadLineExact("CHECKPRESENT-SUCCESS KeyRelative") 572 573 h.requireWriteLine("CHECKPRESENT KeyThatDoesNotExist") 574 h.requireReadLineExact("CHECKPRESENT-FAILURE KeyThatDoesNotExist") 575 576 require.NoError(t, h.mockStdinW.Close()) 577 }, 578 }, 579 { 580 label: "TransferStorePathWithInteriorWhitespace", 581 testProtocolFunc: func(t *testing.T, h *testState) { 582 // Save the current working directory so we can restore it when this 583 // test ends. 584 cwd, err := os.Getwd() 585 require.NoError(t, err) 586 587 require.NoError(t, os.Chdir(t.TempDir())) 588 t.Cleanup(func() { require.NoError(t, os.Chdir(cwd)) }) 589 590 h.preconfigureServer() 591 592 h.requireReadLineExact("VERSION 1") 593 h.requireWriteLine("INITREMOTE") 594 h.requireReadLineExact("INITREMOTE-SUCCESS") 595 596 // Create temp file for transfer. 597 fileToTransfer := "filename with spaces.txt" 598 require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600)) 599 require.FileExists(t, fileToTransfer) 600 require.False(t, filepath.IsAbs(fileToTransfer)) 601 602 // Specify a relative path to transfer. 603 h.requireWriteLine("TRANSFER STORE KeyRelative " + fileToTransfer) 604 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyRelative") 605 require.FileExists(t, filepath.Join(h.localFsDir, "KeyRelative")) 606 607 h.requireWriteLine("CHECKPRESENT KeyRelative") 608 h.requireReadLineExact("CHECKPRESENT-SUCCESS KeyRelative") 609 610 h.requireWriteLine("CHECKPRESENT KeyThatDoesNotExist") 611 h.requireReadLineExact("CHECKPRESENT-FAILURE KeyThatDoesNotExist") 612 613 require.NoError(t, h.mockStdinW.Close()) 614 }, 615 }, 616 { 617 label: "CheckPresentAndTransfer", 618 testProtocolFunc: func(t *testing.T, h *testState) { 619 h.preconfigureServer() 620 621 // Create temp file for transfer. 622 fileToTransfer := filepath.Join(t.TempDir(), "file.txt") 623 require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600)) 624 625 h.requireReadLineExact("VERSION 1") 626 h.requireWriteLine("INITREMOTE") 627 h.requireReadLineExact("INITREMOTE-SUCCESS") 628 629 h.requireWriteLine("CHECKPRESENT KeyThatDoesNotExist") 630 h.requireReadLineExact("CHECKPRESENT-FAILURE KeyThatDoesNotExist") 631 632 // Specify an absolute path to transfer. 633 require.True(t, filepath.IsAbs(fileToTransfer)) 634 h.requireWriteLine("TRANSFER STORE KeyAbsolute " + fileToTransfer) 635 h.requireReadLineExact("TRANSFER-SUCCESS STORE KeyAbsolute") 636 require.FileExists(t, filepath.Join(h.localFsDir, "KeyAbsolute")) 637 638 require.NoError(t, h.mockStdinW.Close()) 639 }, 640 }, 641 // Check whether a key is present, transfer a file with that key, then check 642 // again whether it is present. 643 // 644 // This is a regression test for a bug where the second CHECKPRESENT would 645 // generate the following response: 646 // 647 // CHECKPRESENT-UNKNOWN ${key} failed to read directory entry: readdirent ${filepath}: not a directory 648 // 649 // This message was generated by the local backend's `List()` function. When 650 // checking whether a file exists, we were erroneously listing its contents as 651 // if it were a directory. 652 { 653 label: "CheckpresentTransferCheckpresent", 654 testProtocolFunc: func(t *testing.T, h *testState) { 655 h.preconfigureServer() 656 657 // Create temp file for transfer. 658 fileToTransfer := filepath.Join(t.TempDir(), "file.txt") 659 require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600)) 660 661 h.requireReadLineExact("VERSION 1") 662 h.requireWriteLine("INITREMOTE") 663 h.requireReadLineExact("INITREMOTE-SUCCESS") 664 665 h.requireWriteLine("CHECKPRESENT foo") 666 h.requireReadLineExact("CHECKPRESENT-FAILURE foo") 667 668 h.requireWriteLine("TRANSFER STORE foo " + fileToTransfer) 669 h.requireReadLineExact("TRANSFER-SUCCESS STORE foo") 670 require.FileExists(t, filepath.Join(h.localFsDir, "foo")) 671 672 h.requireWriteLine("CHECKPRESENT foo") 673 h.requireReadLineExact("CHECKPRESENT-SUCCESS foo") 674 675 require.NoError(t, h.mockStdinW.Close()) 676 }, 677 }, 678 { 679 label: "TransferAndCheckpresentWithRealisticKey", 680 testProtocolFunc: func(t *testing.T, h *testState) { 681 h.preconfigureServer() 682 683 // Create temp file for transfer. 684 fileToTransfer := filepath.Join(t.TempDir(), "file.txt") 685 require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600)) 686 687 h.requireReadLineExact("VERSION 1") 688 h.requireWriteLine("INITREMOTE") 689 h.requireReadLineExact("INITREMOTE-SUCCESS") 690 691 realisticKey := "SHA256E-s1048576--7ba87e06b9b7903cfbaf4a38736766c161e3e7b42f06fe57f040aa410a8f0701.this-is-a-test-key" 692 693 // Specify an absolute path to transfer. 694 require.True(t, filepath.IsAbs(fileToTransfer)) 695 h.requireWriteLine(fmt.Sprintf("TRANSFER STORE %s %s", realisticKey, fileToTransfer)) 696 h.requireReadLineExact("TRANSFER-SUCCESS STORE " + realisticKey) 697 require.FileExists(t, filepath.Join(h.localFsDir, realisticKey)) 698 699 h.requireWriteLine("CHECKPRESENT " + realisticKey) 700 h.requireReadLineExact("CHECKPRESENT-SUCCESS " + realisticKey) 701 702 require.NoError(t, h.mockStdinW.Close()) 703 }, 704 }, 705 { 706 label: "RetrieveNonexistentFile", 707 testProtocolFunc: func(t *testing.T, h *testState) { 708 h.preconfigureServer() 709 710 h.requireReadLineExact("VERSION 1") 711 h.requireWriteLine("INITREMOTE") 712 h.requireReadLineExact("INITREMOTE-SUCCESS") 713 714 h.requireWriteLine("TRANSFER RETRIEVE SomeKey path") 715 h.requireReadLineExact("TRANSFER-FAILURE RETRIEVE SomeKey not found") 716 717 require.NoError(t, h.mockStdinW.Close()) 718 }, 719 }, 720 { 721 label: "StoreCheckpresentRetrieve", 722 testProtocolFunc: func(t *testing.T, h *testState) { 723 h.preconfigureServer() 724 725 // Create temp file for transfer. 726 fileToTransfer := filepath.Join(t.TempDir(), "file.txt") 727 require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600)) 728 729 h.requireReadLineExact("VERSION 1") 730 h.requireWriteLine("INITREMOTE") 731 h.requireReadLineExact("INITREMOTE-SUCCESS") 732 733 // Specify an absolute path to transfer. 734 require.True(t, filepath.IsAbs(fileToTransfer)) 735 h.requireWriteLine("TRANSFER STORE SomeKey " + fileToTransfer) 736 h.requireReadLineExact("TRANSFER-SUCCESS STORE SomeKey") 737 require.FileExists(t, filepath.Join(h.localFsDir, "SomeKey")) 738 739 h.requireWriteLine("CHECKPRESENT SomeKey") 740 h.requireReadLineExact("CHECKPRESENT-SUCCESS SomeKey") 741 742 retrievedFilePath := fileToTransfer + ".retrieved" 743 require.NoFileExists(t, retrievedFilePath) 744 h.requireWriteLine("TRANSFER RETRIEVE SomeKey " + retrievedFilePath) 745 h.requireReadLineExact("TRANSFER-SUCCESS RETRIEVE SomeKey") 746 require.FileExists(t, retrievedFilePath) 747 748 require.NoError(t, h.mockStdinW.Close()) 749 }, 750 }, 751 { 752 label: "RemovePreexistingFile", 753 testProtocolFunc: func(t *testing.T, h *testState) { 754 h.preconfigureServer() 755 756 // Write a file into the remote without using the git-annex 757 // protocol. 758 remoteFilePath := filepath.Join(h.localFsDir, "SomeKey") 759 require.NoError(t, os.WriteFile(remoteFilePath, []byte("HELLO"), 0600)) 760 require.FileExists(t, remoteFilePath) 761 762 h.requireReadLineExact("VERSION 1") 763 h.requireWriteLine("INITREMOTE") 764 h.requireReadLineExact("INITREMOTE-SUCCESS") 765 766 h.requireWriteLine("CHECKPRESENT SomeKey") 767 h.requireReadLineExact("CHECKPRESENT-SUCCESS SomeKey") 768 require.FileExists(t, remoteFilePath) 769 770 h.requireWriteLine("REMOVE SomeKey") 771 h.requireReadLineExact("REMOVE-SUCCESS SomeKey") 772 require.NoFileExists(t, remoteFilePath) 773 774 h.requireWriteLine("CHECKPRESENT SomeKey") 775 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey") 776 require.NoFileExists(t, remoteFilePath) 777 778 require.NoError(t, h.mockStdinW.Close()) 779 }, 780 }, 781 { 782 label: "Remove", 783 testProtocolFunc: func(t *testing.T, h *testState) { 784 h.preconfigureServer() 785 786 // Create temp file for transfer. 787 fileToTransfer := filepath.Join(t.TempDir(), "file.txt") 788 require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600)) 789 790 h.requireReadLineExact("VERSION 1") 791 h.requireWriteLine("INITREMOTE") 792 h.requireReadLineExact("INITREMOTE-SUCCESS") 793 794 h.requireWriteLine("CHECKPRESENT SomeKey") 795 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey") 796 797 // Specify an absolute path to transfer. 798 require.True(t, filepath.IsAbs(fileToTransfer)) 799 h.requireWriteLine("TRANSFER STORE SomeKey " + fileToTransfer) 800 h.requireReadLineExact("TRANSFER-SUCCESS STORE SomeKey") 801 require.FileExists(t, filepath.Join(h.localFsDir, "SomeKey")) 802 803 h.requireWriteLine("CHECKPRESENT SomeKey") 804 h.requireReadLineExact("CHECKPRESENT-SUCCESS SomeKey") 805 806 h.requireWriteLine("REMOVE SomeKey") 807 h.requireReadLineExact("REMOVE-SUCCESS SomeKey") 808 require.NoFileExists(t, filepath.Join(h.localFsDir, "SomeKey")) 809 810 h.requireWriteLine("CHECKPRESENT SomeKey") 811 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey") 812 813 require.NoError(t, h.mockStdinW.Close()) 814 }, 815 }, 816 { 817 label: "RemoveNonexistentFile", 818 testProtocolFunc: func(t *testing.T, h *testState) { 819 h.preconfigureServer() 820 821 // Create temp file for transfer. 822 fileToTransfer := filepath.Join(t.TempDir(), "file.txt") 823 require.NoError(t, os.WriteFile(fileToTransfer, []byte("HELLO"), 0600)) 824 825 h.requireReadLineExact("VERSION 1") 826 h.requireWriteLine("INITREMOTE") 827 h.requireReadLineExact("INITREMOTE-SUCCESS") 828 829 h.requireWriteLine("CHECKPRESENT SomeKey") 830 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey") 831 832 require.NoFileExists(t, filepath.Join(h.localFsDir, "SomeKey")) 833 h.requireWriteLine("REMOVE SomeKey") 834 h.requireReadLineExact("REMOVE-SUCCESS SomeKey") 835 require.NoFileExists(t, filepath.Join(h.localFsDir, "SomeKey")) 836 837 h.requireWriteLine("CHECKPRESENT SomeKey") 838 h.requireReadLineExact("CHECKPRESENT-FAILURE SomeKey") 839 840 require.NoError(t, h.mockStdinW.Close()) 841 }, 842 }, 843 { 844 label: "ExportNotSupported", 845 testProtocolFunc: func(t *testing.T, h *testState) { 846 h.preconfigureServer() 847 848 h.requireReadLineExact("VERSION 1") 849 h.requireWriteLine("INITREMOTE") 850 h.requireReadLineExact("INITREMOTE-SUCCESS") 851 852 h.requireWriteLine("EXPORTSUPPORTED") 853 h.requireReadLineExact("EXPORTSUPPORTED-FAILURE") 854 855 require.NoError(t, h.mockStdinW.Close()) 856 }, 857 }, 858 } 859 860 func TestGitAnnexLocalBackendCases(t *testing.T) { 861 for _, testCase := range localBackendTestCases { 862 // Clear global state left behind by tests that chdir to a temp directory. 863 cache.Clear() 864 865 // TODO: Remove this when rclone requires a Go version >= 1.22. Future 866 // versions of Go fix the semantics of capturing a range variable. 867 // https://go.dev/blog/loopvar-preview 868 testCase := testCase 869 870 t.Run(testCase.label, func(t *testing.T) { 871 tempDir := t.TempDir() 872 873 // Create temp dir for an rclone remote pointing at local filesystem. 874 localFsDir := filepath.Join(tempDir, "remoteTarget") 875 require.NoError(t, os.Mkdir(localFsDir, 0700)) 876 877 // Create temp config 878 remoteName := getUniqueRemoteName(t) 879 configLines := []string{ 880 fmt.Sprintf("[%s]", remoteName), 881 "type = local", 882 fmt.Sprintf("remote = %s", localFsDir), 883 } 884 configContents := strings.Join(configLines, "\n") 885 886 configPath := filepath.Join(tempDir, "rclone.conf") 887 require.NoError(t, os.WriteFile(configPath, []byte(configContents), 0600)) 888 require.NoError(t, config.SetConfigPath(configPath)) 889 890 // The custom config file will be ignored unless we install the 891 // global config file handler. 892 configfile.Install() 893 894 handle := makeTestState(t) 895 handle.localFsDir = localFsDir 896 handle.configPath = configPath 897 handle.remoteName = remoteName 898 899 var wg sync.WaitGroup 900 wg.Add(1) 901 902 go func() { 903 err := handle.server.run() 904 905 if testCase.expectedError == "" { 906 require.NoError(t, err) 907 } else { 908 require.ErrorContains(t, err, testCase.expectedError) 909 } 910 911 wg.Done() 912 }() 913 defer wg.Wait() 914 915 testCase.testProtocolFunc(t, &handle) 916 }) 917 } 918 } 919 920 // Configure the git-annex client with a mockfs backend and send it the 921 // "INITREMOTE" command over mocked stdin. This should fail because mockfs does 922 // not support empty directories. 923 func TestGitAnnexHandleInitRemoteBackendDoesNotSupportEmptyDirectories(t *testing.T) { 924 tempDir := t.TempDir() 925 926 // Temporarily override the filesystem registry. 927 oldRegistry := fs.Registry 928 mockfs.Register() 929 defer func() { fs.Registry = oldRegistry }() 930 931 // Create temp dir for an rclone remote pointing at local filesystem. 932 localFsDir := filepath.Join(tempDir, "remoteTarget") 933 require.NoError(t, os.Mkdir(localFsDir, 0700)) 934 935 // Create temp config 936 remoteName := getUniqueRemoteName(t) 937 configLines := []string{ 938 fmt.Sprintf("[%s]", remoteName), 939 "type = mockfs", 940 fmt.Sprintf("remote = %s", localFsDir), 941 } 942 configContents := strings.Join(configLines, "\n") 943 944 configPath := filepath.Join(tempDir, "rclone.conf") 945 require.NoError(t, os.WriteFile(configPath, []byte(configContents), 0600)) 946 947 // The custom config file will be ignored unless we install the global 948 // config file handler. 949 configfile.Install() 950 require.NoError(t, config.SetConfigPath(configPath)) 951 952 handle := makeTestState(t) 953 handle.server.configPrefix = localFsDir 954 handle.server.configRcloneRemoteName = remoteName 955 handle.server.configsDone = true 956 957 var wg sync.WaitGroup 958 wg.Add(1) 959 960 go func() { 961 require.NotNil(t, handle.server.run()) 962 wg.Done() 963 }() 964 defer wg.Wait() 965 966 handle.requireReadLineExact("VERSION 1") 967 handle.requireWriteLine("INITREMOTE") 968 handle.requireReadLineExact("INITREMOTE-FAILURE this rclone remote does not support empty directories") 969 }