2018-10-28 15:31:24 +01:00
|
|
|
package rcserver
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2020-11-05 16:18:51 +01:00
|
|
|
"context"
|
2024-04-05 22:06:41 +02:00
|
|
|
"encoding/json"
|
2018-10-28 15:31:24 +01:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
2021-01-10 13:24:22 +01:00
|
|
|
"os"
|
2023-09-25 20:03:38 +02:00
|
|
|
"path/filepath"
|
2018-10-28 15:31:24 +01:00
|
|
|
"regexp"
|
2019-06-10 12:59:06 +02:00
|
|
|
"strings"
|
2018-10-28 15:31:24 +01:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2020-02-26 09:34:32 +01:00
|
|
|
_ "github.com/rclone/rclone/backend/local"
|
2021-01-10 13:24:22 +01:00
|
|
|
"github.com/rclone/rclone/fs"
|
2020-02-26 09:34:32 +01:00
|
|
|
"github.com/rclone/rclone/fs/accounting"
|
2021-03-10 16:40:34 +01:00
|
|
|
"github.com/rclone/rclone/fs/config/configfile"
|
2020-02-26 09:34:32 +01:00
|
|
|
"github.com/rclone/rclone/fs/rc"
|
2022-12-11 15:47:47 +01:00
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
2018-10-28 15:31:24 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2023-09-25 20:03:38 +02:00
|
|
|
testBindAddress = "localhost:0"
|
|
|
|
defaultTestTemplate = "testdata/golden/testindex.html"
|
|
|
|
testFs = "testdata/files"
|
|
|
|
remoteURL = "[" + testFs + "]/" // initial URL path to fetch from that remote
|
2018-10-28 15:31:24 +01:00
|
|
|
)
|
|
|
|
|
2021-01-10 13:24:22 +01:00
|
|
|
func TestMain(m *testing.M) {
|
|
|
|
// Pretend to be rclone version if we have a version string parameter
|
|
|
|
if os.Args[len(os.Args)-1] == "version" {
|
|
|
|
fmt.Printf("rclone %s\n", fs.Version)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
// Pretend to error if we have an unknown command
|
|
|
|
if os.Args[len(os.Args)-1] == "unknown_command" {
|
|
|
|
fmt.Printf("rclone %s\n", fs.Version)
|
|
|
|
fmt.Fprintf(os.Stderr, "Unknown command\n")
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
os.Exit(m.Run())
|
|
|
|
}
|
|
|
|
|
2018-10-28 15:31:24 +01:00
|
|
|
// Test the RC server runs and we can do HTTP fetches from it.
|
|
|
|
// We'll do the majority of the testing with the httptest framework
|
|
|
|
func TestRcServer(t *testing.T) {
|
2024-07-04 11:47:05 +02:00
|
|
|
opt := rc.Opt
|
2022-12-11 15:47:47 +01:00
|
|
|
opt.HTTP.ListenAddr = []string{testBindAddress}
|
2023-09-25 20:03:38 +02:00
|
|
|
opt.Template.Path = defaultTestTemplate
|
2018-10-28 15:31:24 +01:00
|
|
|
opt.Enabled = true
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = testFs
|
|
|
|
mux := http.NewServeMux()
|
2022-12-11 15:47:47 +01:00
|
|
|
rcServer, err := newServer(context.Background(), &opt, mux)
|
|
|
|
require.NoError(t, err)
|
2018-11-01 18:20:04 +01:00
|
|
|
assert.NoError(t, rcServer.Serve())
|
|
|
|
defer func() {
|
2022-12-11 15:47:47 +01:00
|
|
|
assert.NoError(t, rcServer.Shutdown())
|
2018-11-01 18:20:04 +01:00
|
|
|
rcServer.Wait()
|
|
|
|
}()
|
2022-12-11 15:47:47 +01:00
|
|
|
testURL := rcServer.server.URLs()[0]
|
2018-10-28 15:31:24 +01:00
|
|
|
|
|
|
|
// Do the simplest possible test to check the server is alive
|
|
|
|
// Do it a few times to wait for the server to start
|
|
|
|
var resp *http.Response
|
|
|
|
for i := 0; i < 10; i++ {
|
|
|
|
resp, err = http.Get(testURL + "file.txt")
|
|
|
|
if err == nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
}
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
2022-08-20 16:38:02 +02:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2018-10-28 15:31:24 +01:00
|
|
|
_ = resp.Body.Close()
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, resp.Body.Close())
|
|
|
|
|
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
assert.Equal(t, "this is file1.txt\n", string(body))
|
|
|
|
}
|
|
|
|
|
|
|
|
type testRun struct {
|
|
|
|
Name string
|
|
|
|
URL string
|
2022-12-11 15:47:47 +01:00
|
|
|
User string
|
|
|
|
Pass string
|
2018-10-28 15:31:24 +01:00
|
|
|
Status int
|
|
|
|
Method string
|
|
|
|
Range string
|
|
|
|
Body string
|
|
|
|
ContentType string
|
|
|
|
Expected string
|
|
|
|
Contains *regexp.Regexp
|
|
|
|
Headers map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run a suite of tests
|
|
|
|
func testServer(t *testing.T, tests []testRun, opt *rc.Options) {
|
2023-09-25 20:03:38 +02:00
|
|
|
t.Helper()
|
|
|
|
|
2021-03-10 16:40:34 +01:00
|
|
|
ctx := context.Background()
|
2021-04-26 23:37:49 +02:00
|
|
|
configfile.Install()
|
2023-09-25 20:03:38 +02:00
|
|
|
if opt.Template.Path == "" {
|
|
|
|
opt.Template.Path = defaultTestTemplate
|
|
|
|
}
|
2022-12-11 15:47:47 +01:00
|
|
|
rcServer, err := newServer(ctx, opt, http.DefaultServeMux)
|
|
|
|
require.NoError(t, err)
|
|
|
|
testURL := rcServer.server.URLs()[0]
|
|
|
|
mux := rcServer.server.Router()
|
2018-10-28 15:31:24 +01:00
|
|
|
for _, test := range tests {
|
|
|
|
t.Run(test.Name, func(t *testing.T) {
|
2023-09-25 20:03:38 +02:00
|
|
|
t.Helper()
|
|
|
|
|
2018-10-28 15:31:24 +01:00
|
|
|
method := test.Method
|
|
|
|
if method == "" {
|
|
|
|
method = "GET"
|
|
|
|
}
|
|
|
|
var inBody io.Reader
|
|
|
|
if test.Body != "" {
|
|
|
|
buf := bytes.NewBufferString(test.Body)
|
|
|
|
inBody = buf
|
|
|
|
}
|
|
|
|
req, err := http.NewRequest(method, "http://1.2.3.4/"+test.URL, inBody)
|
|
|
|
require.NoError(t, err)
|
|
|
|
if test.Range != "" {
|
|
|
|
req.Header.Add("Range", test.Range)
|
|
|
|
}
|
|
|
|
if test.ContentType != "" {
|
|
|
|
req.Header.Add("Content-Type", test.ContentType)
|
|
|
|
}
|
2022-12-11 15:47:47 +01:00
|
|
|
if test.User != "" && test.Pass != "" {
|
|
|
|
req.SetBasicAuth(test.User, test.Pass)
|
|
|
|
}
|
2018-10-28 15:31:24 +01:00
|
|
|
|
|
|
|
w := httptest.NewRecorder()
|
2022-12-11 15:47:47 +01:00
|
|
|
mux.ServeHTTP(w, req)
|
2018-10-28 15:31:24 +01:00
|
|
|
resp := w.Result()
|
|
|
|
|
|
|
|
assert.Equal(t, test.Status, resp.StatusCode)
|
2022-08-20 16:38:02 +02:00
|
|
|
body, err := io.ReadAll(resp.Body)
|
2018-10-28 15:31:24 +01:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-04-05 22:06:41 +02:00
|
|
|
if test.ContentType == "application/json" && test.Expected != "" {
|
|
|
|
expectedNormalized := normalizeJSON(t, test.Expected)
|
|
|
|
actualNormalized := normalizeJSON(t, string(body))
|
|
|
|
assert.Equal(t, expectedNormalized, actualNormalized, "Normalized JSON does not match")
|
|
|
|
} else if test.Contains == nil {
|
2018-10-28 15:31:24 +01:00
|
|
|
assert.Equal(t, test.Expected, string(body))
|
|
|
|
} else {
|
|
|
|
assert.True(t, test.Contains.Match(body), fmt.Sprintf("body didn't match: %v: %v", test.Contains, string(body)))
|
|
|
|
}
|
|
|
|
|
|
|
|
for k, v := range test.Headers {
|
2022-12-11 15:47:47 +01:00
|
|
|
if v == "testURL" {
|
|
|
|
v = testURL
|
|
|
|
}
|
2018-10-28 15:31:24 +01:00
|
|
|
assert.Equal(t, v, resp.Header.Get(k), k)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// return an enabled rc
|
|
|
|
func newTestOpt() rc.Options {
|
2024-07-04 11:47:05 +02:00
|
|
|
opt := rc.Opt
|
2018-10-28 15:31:24 +01:00
|
|
|
opt.Enabled = true
|
2022-12-11 15:47:47 +01:00
|
|
|
opt.HTTP.ListenAddr = []string{testBindAddress}
|
2018-10-28 15:31:24 +01:00
|
|
|
return opt
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFileServing(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "index",
|
|
|
|
URL: "",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: `<pre>
|
|
|
|
<a href="dir/">dir/</a>
|
|
|
|
<a href="file.txt">file.txt</a>
|
2023-09-25 20:03:38 +02:00
|
|
|
<a href="modtime/">modtime/</a>
|
2018-10-28 15:31:24 +01:00
|
|
|
</pre>
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "notfound",
|
|
|
|
URL: "notfound",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: "404 page not found\n",
|
|
|
|
}, {
|
|
|
|
Name: "dirnotfound",
|
|
|
|
URL: "dirnotfound/",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: "404 page not found\n",
|
|
|
|
}, {
|
|
|
|
Name: "dir",
|
|
|
|
URL: "dir/",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: `<pre>
|
|
|
|
<a href="file2.txt">file2.txt</a>
|
|
|
|
</pre>
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "file",
|
|
|
|
URL: "file.txt",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "this is file1.txt\n",
|
|
|
|
Headers: map[string]string{
|
|
|
|
"Content-Length": "18",
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "file2",
|
|
|
|
URL: "dir/file2.txt",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "this is dir/file2.txt\n",
|
|
|
|
}, {
|
|
|
|
Name: "file-head",
|
|
|
|
URL: "file.txt",
|
|
|
|
Method: "HEAD",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: ``,
|
|
|
|
Headers: map[string]string{
|
|
|
|
"Content-Length": "18",
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "file-range",
|
|
|
|
URL: "file.txt",
|
|
|
|
Status: http.StatusPartialContent,
|
|
|
|
Range: "bytes=8-12",
|
|
|
|
Expected: `file1`,
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = testFs
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRemoteServing(t *testing.T) {
|
|
|
|
tests := []testRun{
|
|
|
|
// Test serving files from the test remote
|
|
|
|
{
|
|
|
|
Name: "index",
|
|
|
|
URL: remoteURL + "",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: `<!DOCTYPE html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<title>Directory listing of /</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Directory listing of /</h1>
|
|
|
|
<a href="dir/">dir/</a><br />
|
2023-09-25 20:03:38 +02:00
|
|
|
<a href="modtime/">modtime/</a><br />
|
2018-10-28 15:31:24 +01:00
|
|
|
<a href="file.txt">file.txt</a><br />
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "notfound-index",
|
|
|
|
URL: "[notfound]/",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: `{
|
|
|
|
"error": "failed to list directory: directory not found",
|
|
|
|
"input": null,
|
|
|
|
"path": "",
|
|
|
|
"status": 404
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "notfound",
|
|
|
|
URL: remoteURL + "notfound",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: `{
|
|
|
|
"error": "failed to find object: object not found",
|
|
|
|
"input": null,
|
2019-06-10 12:59:06 +02:00
|
|
|
"path": "notfound",
|
2018-10-28 15:31:24 +01:00
|
|
|
"status": 404
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "dirnotfound",
|
|
|
|
URL: remoteURL + "dirnotfound/",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: `{
|
|
|
|
"error": "failed to list directory: directory not found",
|
|
|
|
"input": null,
|
|
|
|
"path": "dirnotfound",
|
|
|
|
"status": 404
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "dir",
|
|
|
|
URL: remoteURL + "dir/",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: `<!DOCTYPE html>
|
|
|
|
<html lang="en">
|
|
|
|
<head>
|
|
|
|
<meta charset="utf-8">
|
|
|
|
<title>Directory listing of /dir</title>
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<h1>Directory listing of /dir</h1>
|
|
|
|
<a href="file2.txt">file2.txt</a><br />
|
|
|
|
</body>
|
|
|
|
</html>
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "file",
|
|
|
|
URL: remoteURL + "file.txt",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "this is file1.txt\n",
|
|
|
|
Headers: map[string]string{
|
|
|
|
"Content-Length": "18",
|
|
|
|
},
|
2019-06-10 12:59:06 +02:00
|
|
|
}, {
|
|
|
|
Name: "file with no slash after ]",
|
|
|
|
URL: strings.TrimRight(remoteURL, "/") + "file.txt",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "this is file1.txt\n",
|
|
|
|
Headers: map[string]string{
|
|
|
|
"Content-Length": "18",
|
|
|
|
},
|
2018-10-28 15:31:24 +01:00
|
|
|
}, {
|
|
|
|
Name: "file2",
|
|
|
|
URL: remoteURL + "dir/file2.txt",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "this is dir/file2.txt\n",
|
|
|
|
}, {
|
|
|
|
Name: "file-head",
|
|
|
|
URL: remoteURL + "file.txt",
|
|
|
|
Method: "HEAD",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: ``,
|
|
|
|
Headers: map[string]string{
|
|
|
|
"Content-Length": "18",
|
|
|
|
},
|
|
|
|
}, {
|
|
|
|
Name: "file-range",
|
|
|
|
URL: remoteURL + "file.txt",
|
|
|
|
Status: http.StatusPartialContent,
|
|
|
|
Range: "bytes=8-12",
|
|
|
|
Expected: `file1`,
|
|
|
|
}, {
|
|
|
|
Name: "bad-remote",
|
|
|
|
URL: "[notfoundremote:]/",
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Expected: `{
|
|
|
|
"error": "failed to make Fs: didn't find section in config file",
|
|
|
|
"input": null,
|
|
|
|
"path": "/",
|
|
|
|
"status": 500
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = testFs
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestRC(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "rc-root",
|
|
|
|
URL: "",
|
|
|
|
Method: "POST",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: `{
|
|
|
|
"error": "couldn't find method \"\"",
|
|
|
|
"input": {},
|
|
|
|
"path": "",
|
|
|
|
"status": 404
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "rc-noop",
|
|
|
|
URL: "rc/noop",
|
|
|
|
Method: "POST",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "{}\n",
|
|
|
|
}, {
|
|
|
|
Name: "rc-error",
|
|
|
|
URL: "rc/error",
|
|
|
|
Method: "POST",
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Expected: `{
|
|
|
|
"error": "arbitrary error on input map[]",
|
|
|
|
"input": {},
|
|
|
|
"path": "rc/error",
|
|
|
|
"status": 500
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "core-gc",
|
|
|
|
URL: "core/gc", // returns nil, nil so check it is made into {}
|
|
|
|
Method: "POST",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "{}\n",
|
|
|
|
}, {
|
|
|
|
Name: "url-params",
|
|
|
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
|
|
|
Method: "POST",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: `{
|
|
|
|
"param1": "potato",
|
|
|
|
"param2": "sausage"
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "json",
|
|
|
|
URL: "rc/noop",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{ "param1":"string", "param2":true }`,
|
|
|
|
ContentType: "application/json",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: `{
|
|
|
|
"param1": "string",
|
|
|
|
"param2": true
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "json-and-url-params",
|
|
|
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{ "param1":"string", "param3":true }`,
|
|
|
|
ContentType: "application/json",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: `{
|
|
|
|
"param1": "string",
|
|
|
|
"param2": "sausage",
|
|
|
|
"param3": true
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "json-bad",
|
|
|
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{ param1":"string", "param3":true }`,
|
|
|
|
ContentType: "application/json",
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Expected: `{
|
|
|
|
"error": "failed to read input JSON: invalid character 'p' looking for beginning of object key string",
|
|
|
|
"input": {
|
|
|
|
"param1": "potato",
|
|
|
|
"param2": "sausage"
|
|
|
|
},
|
|
|
|
"path": "rc/noop",
|
|
|
|
"status": 400
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "form",
|
|
|
|
URL: "rc/noop",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `param1=string¶m2=true`,
|
|
|
|
ContentType: "application/x-www-form-urlencoded",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: `{
|
|
|
|
"param1": "string",
|
|
|
|
"param2": "true"
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "form-and-url-params",
|
|
|
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `param1=string¶m3=true`,
|
|
|
|
ContentType: "application/x-www-form-urlencoded",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: `{
|
|
|
|
"param1": "potato",
|
|
|
|
"param2": "sausage",
|
|
|
|
"param3": "true"
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "form-bad",
|
|
|
|
URL: "rc/noop?param1=potato¶m2=sausage",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `%zz`,
|
|
|
|
ContentType: "application/x-www-form-urlencoded",
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Expected: `{
|
|
|
|
"error": "failed to parse form/URL parameters: invalid URL escape \"%zz\"",
|
|
|
|
"input": null,
|
|
|
|
"path": "rc/noop",
|
|
|
|
"status": 400
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = testFs
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
2021-01-10 13:24:22 +01:00
|
|
|
func TestRCWithAuth(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "core-command",
|
|
|
|
URL: "core/command",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `command=version`,
|
|
|
|
ContentType: "application/x-www-form-urlencoded",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: fmt.Sprintf(`{
|
|
|
|
"error": false,
|
|
|
|
"result": "rclone %s\n"
|
|
|
|
}
|
|
|
|
`, fs.Version),
|
|
|
|
}, {
|
|
|
|
Name: "core-command-bad-returnType",
|
|
|
|
URL: "core/command",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `command=version&returnType=POTATO`,
|
|
|
|
ContentType: "application/x-www-form-urlencoded",
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Expected: `{
|
2022-06-08 22:54:39 +02:00
|
|
|
"error": "unknown returnType \"POTATO\"",
|
2021-01-10 13:24:22 +01:00
|
|
|
"input": {
|
|
|
|
"command": "version",
|
|
|
|
"returnType": "POTATO"
|
|
|
|
},
|
|
|
|
"path": "core/command",
|
|
|
|
"status": 500
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}, {
|
|
|
|
Name: "core-command-stream",
|
|
|
|
URL: "core/command",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `command=version&returnType=STREAM`,
|
|
|
|
ContentType: "application/x-www-form-urlencoded",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: fmt.Sprintf(`rclone %s
|
|
|
|
{}
|
|
|
|
`, fs.Version),
|
|
|
|
}, {
|
|
|
|
Name: "core-command-stream-error",
|
|
|
|
URL: "core/command",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `command=unknown_command&returnType=STREAM`,
|
|
|
|
ContentType: "application/x-www-form-urlencoded",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: fmt.Sprintf(`rclone %s
|
|
|
|
Unknown command
|
|
|
|
{
|
|
|
|
"error": "exit status 1",
|
|
|
|
"input": {
|
|
|
|
"command": "unknown_command",
|
|
|
|
"returnType": "STREAM"
|
|
|
|
},
|
|
|
|
"path": "core/command",
|
|
|
|
"status": 500
|
|
|
|
}
|
|
|
|
`, fs.Version),
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = testFs
|
|
|
|
opt.NoAuth = true
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
2020-02-26 09:34:32 +01:00
|
|
|
func TestMetrics(t *testing.T) {
|
|
|
|
stats := accounting.GlobalStats()
|
|
|
|
tests := makeMetricsTestCases(stats)
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.EnableMetrics = true
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
|
|
|
|
// Test changing a couple options
|
|
|
|
stats.Bytes(500)
|
2023-03-08 19:40:37 +01:00
|
|
|
for i := 0; i < 30; i++ {
|
|
|
|
require.NoError(t, stats.DeleteFile(context.Background(), 0))
|
|
|
|
}
|
2020-02-26 09:34:32 +01:00
|
|
|
stats.Errors(2)
|
|
|
|
stats.Bytes(324)
|
|
|
|
|
|
|
|
tests = makeMetricsTestCases(stats)
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeMetricsTestCases(stats *accounting.StatsInfo) (tests []testRun) {
|
|
|
|
tests = []testRun{{
|
|
|
|
Name: "Bytes Transferred Metric",
|
|
|
|
URL: "/metrics",
|
|
|
|
Method: "GET",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Contains: regexp.MustCompile(fmt.Sprintf("rclone_bytes_transferred_total %d", stats.GetBytes())),
|
|
|
|
}, {
|
|
|
|
Name: "Checked Files Metric",
|
|
|
|
URL: "/metrics",
|
|
|
|
Method: "GET",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Contains: regexp.MustCompile(fmt.Sprintf("rclone_checked_files_total %d", stats.GetChecks())),
|
|
|
|
}, {
|
|
|
|
Name: "Errors Metric",
|
|
|
|
URL: "/metrics",
|
|
|
|
Method: "GET",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Contains: regexp.MustCompile(fmt.Sprintf("rclone_errors_total %d", stats.GetErrors())),
|
|
|
|
}, {
|
|
|
|
Name: "Deleted Files Metric",
|
|
|
|
URL: "/metrics",
|
|
|
|
Method: "GET",
|
|
|
|
Status: http.StatusOK,
|
2023-03-08 19:40:37 +01:00
|
|
|
Contains: regexp.MustCompile(fmt.Sprintf("rclone_files_deleted_total %d", stats.GetDeletes())),
|
2020-02-26 09:34:32 +01:00
|
|
|
}, {
|
|
|
|
Name: "Files Transferred Metric",
|
|
|
|
URL: "/metrics",
|
|
|
|
Method: "GET",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Contains: regexp.MustCompile(fmt.Sprintf("rclone_files_transferred_total %d", stats.GetTransfers())),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-05-08 17:15:21 +02:00
|
|
|
var matchRemoteDirListing = regexp.MustCompile(`<title>Directory listing of /</title>`)
|
2018-10-28 15:31:24 +01:00
|
|
|
|
|
|
|
func TestServingRoot(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "rootlist",
|
|
|
|
URL: "*",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Contains: matchRemoteDirListing,
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = testFs
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestServingRootNoFiles(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "rootlist",
|
|
|
|
URL: "",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Contains: matchRemoteDirListing,
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = ""
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestNoFiles(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "file",
|
|
|
|
URL: "file.txt",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: "Not Found\n",
|
|
|
|
}, {
|
|
|
|
Name: "dir",
|
|
|
|
URL: "dir/",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: "Not Found\n",
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = ""
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestNoServe(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "file",
|
|
|
|
URL: remoteURL + "file.txt",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: "404 page not found\n",
|
|
|
|
}, {
|
|
|
|
Name: "dir",
|
|
|
|
URL: remoteURL + "dir/",
|
|
|
|
Status: http.StatusNotFound,
|
|
|
|
Expected: "404 page not found\n",
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = false
|
|
|
|
opt.Files = testFs
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
2018-11-03 17:37:09 +01:00
|
|
|
func TestAuthRequired(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "auth",
|
|
|
|
URL: "rc/noopauth",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{}`,
|
|
|
|
ContentType: "application/javascript",
|
|
|
|
Status: http.StatusForbidden,
|
|
|
|
Expected: `{
|
|
|
|
"error": "authentication must be set up on the rc server to use \"rc/noopauth\" or the --rc-no-auth flag must be in use",
|
|
|
|
"input": {},
|
|
|
|
"path": "rc/noopauth",
|
|
|
|
"status": 403
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = false
|
|
|
|
opt.Files = ""
|
|
|
|
opt.NoAuth = false
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestNoAuth(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "auth",
|
|
|
|
URL: "rc/noopauth",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{}`,
|
|
|
|
ContentType: "application/javascript",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "{}\n",
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = false
|
|
|
|
opt.Files = ""
|
|
|
|
opt.NoAuth = true
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestWithUserPass(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
2022-12-11 15:47:47 +01:00
|
|
|
Name: "authMissing",
|
|
|
|
URL: "rc/noopauth",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{}`,
|
|
|
|
ContentType: "application/javascript",
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Expected: "401 Unauthorized\n",
|
|
|
|
}, {
|
|
|
|
Name: "authWrong",
|
|
|
|
URL: "rc/noopauth",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{}`,
|
|
|
|
ContentType: "application/javascript",
|
|
|
|
Status: http.StatusUnauthorized,
|
|
|
|
Expected: "401 Unauthorized\n",
|
|
|
|
User: "user1",
|
|
|
|
Pass: "pass2",
|
|
|
|
}, {
|
|
|
|
Name: "authOK",
|
2018-11-03 17:37:09 +01:00
|
|
|
URL: "rc/noopauth",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{}`,
|
|
|
|
ContentType: "application/javascript",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "{}\n",
|
2022-12-11 15:47:47 +01:00
|
|
|
User: "user",
|
|
|
|
Pass: "pass",
|
2018-11-03 17:37:09 +01:00
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = false
|
|
|
|
opt.Files = ""
|
|
|
|
opt.NoAuth = false
|
2022-12-11 15:47:47 +01:00
|
|
|
opt.Auth.BasicUser = "user"
|
|
|
|
opt.Auth.BasicPass = "pass"
|
2018-11-03 17:37:09 +01:00
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
2018-10-28 15:31:24 +01:00
|
|
|
func TestRCAsync(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "ok",
|
|
|
|
URL: "rc/noop",
|
|
|
|
Method: "POST",
|
|
|
|
ContentType: "application/json",
|
|
|
|
Body: `{ "_async":true }`,
|
|
|
|
Status: http.StatusOK,
|
2019-07-18 12:13:54 +02:00
|
|
|
Contains: regexp.MustCompile(`(?s)\{.*\"jobid\":.*\}`),
|
2018-10-28 15:31:24 +01:00
|
|
|
}, {
|
|
|
|
Name: "bad",
|
|
|
|
URL: "rc/noop",
|
|
|
|
Method: "POST",
|
|
|
|
ContentType: "application/json",
|
|
|
|
Body: `{ "_async":"truthy" }`,
|
|
|
|
Status: http.StatusBadRequest,
|
|
|
|
Expected: `{
|
|
|
|
"error": "couldn't parse key \"_async\" (truthy) as bool: strconv.ParseBool: parsing \"truthy\": invalid syntax",
|
|
|
|
"input": {
|
|
|
|
"_async": "truthy"
|
|
|
|
},
|
|
|
|
"path": "rc/noop",
|
|
|
|
"status": 400
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = ""
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
2022-12-11 15:47:47 +01:00
|
|
|
|
|
|
|
// Check the debug handlers are attached
|
|
|
|
func TestRCDebug(t *testing.T) {
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "index",
|
|
|
|
URL: "debug/pprof/",
|
|
|
|
Method: "GET",
|
|
|
|
ContentType: "text/html",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Contains: regexp.MustCompile(`Types of profiles available`),
|
|
|
|
}, {
|
|
|
|
Name: "goroutines",
|
|
|
|
URL: "debug/pprof/goroutine?debug=1",
|
|
|
|
Method: "GET",
|
|
|
|
ContentType: "text/html",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Contains: regexp.MustCompile(`goroutine profile`),
|
|
|
|
}}
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Files = ""
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
2023-09-25 20:03:38 +02:00
|
|
|
|
|
|
|
func TestServeModTime(t *testing.T) {
|
|
|
|
for file, mtime := range map[string]time.Time{
|
|
|
|
"dir": time.Date(2023, 4, 12, 21, 15, 17, 0, time.UTC),
|
|
|
|
"modtime.txt": time.Date(2021, 1, 18, 5, 2, 28, 0, time.UTC),
|
|
|
|
} {
|
|
|
|
path := filepath.Join(testFs, "modtime", file)
|
|
|
|
err := os.Chtimes(path, mtime, mtime)
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
opt := newTestOpt()
|
|
|
|
opt.Serve = true
|
|
|
|
opt.Template.Path = "testdata/golden/testmodtime.html"
|
|
|
|
|
|
|
|
tests := []testRun{{
|
|
|
|
Name: "modtime",
|
|
|
|
Method: "GET",
|
|
|
|
URL: remoteURL + "modtime/",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "* dir/ - 2023-04-12T21:15:17Z\n* modtime.txt - 2021-01-18T05:02:28Z\n",
|
|
|
|
}}
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
|
|
|
|
opt.ServeNoModTime = true
|
|
|
|
tests = []testRun{{
|
|
|
|
Name: "no modtime",
|
|
|
|
Method: "GET",
|
|
|
|
URL: remoteURL + "modtime/",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "* dir/ - 0001-01-01T00:00:00Z\n* modtime.txt - 0001-01-01T00:00:00Z\n",
|
|
|
|
}}
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
2024-04-05 22:06:41 +02:00
|
|
|
|
|
|
|
func TestContentTypeJSON(t *testing.T) {
|
|
|
|
tests := []testRun{
|
|
|
|
{
|
|
|
|
Name: "Check Content-Type for JSON response",
|
|
|
|
URL: "rc/noop",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{}`,
|
|
|
|
ContentType: "application/json",
|
|
|
|
Status: http.StatusOK,
|
|
|
|
Expected: "{}\n",
|
|
|
|
Headers: map[string]string{
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Name: "Check Content-Type for JSON error response",
|
|
|
|
URL: "rc/error",
|
|
|
|
Method: "POST",
|
|
|
|
Body: `{}`,
|
|
|
|
ContentType: "application/json",
|
|
|
|
Status: http.StatusInternalServerError,
|
|
|
|
Expected: `{
|
|
|
|
"error": "arbitrary error on input map[]",
|
|
|
|
"input": {},
|
|
|
|
"path": "rc/error",
|
|
|
|
"status": 500
|
|
|
|
}
|
|
|
|
`,
|
|
|
|
Headers: map[string]string{
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
opt := newTestOpt()
|
|
|
|
testServer(t, tests, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func normalizeJSON(t *testing.T, jsonStr string) string {
|
|
|
|
var jsonObj map[string]interface{}
|
|
|
|
err := json.Unmarshal([]byte(jsonStr), &jsonObj)
|
|
|
|
require.NoError(t, err, "JSON unmarshalling failed")
|
|
|
|
normalizedJSON, err := json.Marshal(jsonObj)
|
|
|
|
require.NoError(t, err, "JSON marshalling failed")
|
|
|
|
return string(normalizedJSON)
|
|
|
|
}
|