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  }