rclone/cmd/serve/restic/restic_test.go
Michael Eischer ef2c5a1998
serve restic: fix error handling
* serve restic: return internal error if listing failed

If listing a remote failed, then rclone returned http status "not
found". This has become a problem since restic 0.16.0 which ignores "not
found"-errors while listing a directory.

Just return internal server error, if something unexpected happens while
listing a directory.

* serve restic: fix error handling if getting a file fails

If the call to `newObject` in `serveObject` fails, then rclone always
returned a "not found" error. This prevents restic from distinguishing
permanent "not found" errors from everything else.

Thus, only return "not found" if the object is not found and an internal
server error otherwise.
2024-01-29 17:54:23 +00:00

172 lines
4.3 KiB
Go

// Serve restic tests set up a server and run the integration tests
// for restic against it.
package restic
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"testing"
_ "github.com/rclone/rclone/backend/all"
"github.com/rclone/rclone/cmd"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fstest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
testBindAddress = "localhost:0"
resticSource = "../../../../../restic/restic"
)
func newOpt() Options {
opt := DefaultOpt
opt.HTTP.ListenAddr = []string{testBindAddress}
return opt
}
// TestRestic runs the restic server then runs the unit tests for the
// restic remote against it.
//
// Requires the restic source code in the location indicated by resticSource.
func TestResticIntegration(t *testing.T) {
ctx := context.Background()
_, err := os.Stat(resticSource)
if err != nil {
t.Skipf("Skipping test as restic source not found: %v", err)
}
opt := newOpt()
fstest.Initialise()
fremote, _, clean, err := fstest.RandomRemote()
assert.NoError(t, err)
defer clean()
err = fremote.Mkdir(context.Background(), "")
assert.NoError(t, err)
// Start the server
s, err := newServer(ctx, fremote, &opt)
require.NoError(t, err)
testURL := s.Server.URLs()[0]
defer func() {
_ = s.Shutdown()
}()
// Change directory to run the tests
err = os.Chdir(resticSource)
require.NoError(t, err, "failed to cd to restic source code")
// Run the restic tests
runTests := func(path string) {
args := []string{"test", "./internal/backend/rest", "-run", "TestBackendRESTExternalServer", "-count=1"}
if testing.Verbose() {
args = append(args, "-v")
}
cmd := exec.Command("go", args...)
cmd.Env = append(os.Environ(),
"RESTIC_TEST_REST_REPOSITORY=rest:"+testURL+path,
"GO111MODULE=on",
)
out, err := cmd.CombinedOutput()
if len(out) != 0 {
t.Logf("\n----------\n%s----------\n", string(out))
}
assert.NoError(t, err, "Running restic integration tests")
}
// Run the tests with no path
runTests("")
//... and again with a path
runTests("potato/sausage/")
}
func TestMakeRemote(t *testing.T) {
for _, test := range []struct {
in, want string
}{
{"/", ""},
{"/data", "data"},
{"/data/", "data"},
{"/data/1", "data/1"},
{"/data/12", "data/12/12"},
{"/data/123", "data/12/123"},
{"/data/123/", "data/12/123"},
{"/keys", "keys"},
{"/keys/1", "keys/1"},
{"/keys/12", "keys/12"},
{"/keys/123", "keys/123"},
} {
r := httptest.NewRequest("GET", test.in, nil)
w := httptest.NewRecorder()
next := http.HandlerFunc(func(_ http.ResponseWriter, request *http.Request) {
remote, ok := request.Context().Value(ContextRemoteKey).(string)
assert.True(t, ok, "Failed to get remote from context")
assert.Equal(t, test.want, remote, test.in)
})
got := WithRemote(next)
got.ServeHTTP(w, r)
}
}
type listErrorFs struct {
fs.Fs
}
func (f *listErrorFs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
return fs.DirEntries{}, errors.New("oops")
}
func TestListErrors(t *testing.T) {
ctx := context.Background()
// setup rclone with a local backend in a temporary directory
tempdir := t.TempDir()
opt := newOpt()
// make a new file system in the temp dir
f := &listErrorFs{Fs: cmd.NewFsSrc([]string{tempdir})}
s, err := newServer(ctx, f, &opt)
require.NoError(t, err)
router := s.Server.Router()
req := newRequest(t, "GET", "/test/snapshots/", nil)
checkRequest(t, router.ServeHTTP, req, []wantFunc{wantCode(http.StatusInternalServerError)})
}
type newObjectErrorFs struct {
fs.Fs
err error
}
func (f *newObjectErrorFs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
return nil, f.err
}
func TestServeErrors(t *testing.T) {
ctx := context.Background()
// setup rclone with a local backend in a temporary directory
tempdir := t.TempDir()
opt := newOpt()
// make a new file system in the temp dir
f := &newObjectErrorFs{Fs: cmd.NewFsSrc([]string{tempdir})}
s, err := newServer(ctx, f, &opt)
require.NoError(t, err)
router := s.Server.Router()
f.err = errors.New("oops")
req := newRequest(t, "GET", "/test/config", nil)
checkRequest(t, router.ServeHTTP, req, []wantFunc{wantCode(http.StatusInternalServerError)})
f.err = fs.ErrorObjectNotFound
checkRequest(t, router.ServeHTTP, req, []wantFunc{wantCode(http.StatusNotFound)})
}