333 lines
11 KiB
Go
333 lines
11 KiB
Go
package protontest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rclone/rclone/backend/protondrive/api"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const testAppVersion = "external-drive-rclone@1.77.0-stable"
|
|
|
|
// do sends a request with the given headers and returns the response
|
|
// and its body.
|
|
func do(t *testing.T, s *Server, method, path string, header map[string]string) (*http.Response, []byte) {
|
|
t.Helper()
|
|
req, err := http.NewRequest(method, s.URL()+path, nil)
|
|
require.NoError(t, err)
|
|
for k, v := range header {
|
|
req.Header.Set(k, v)
|
|
}
|
|
resp, err := s.Client().Do(req)
|
|
require.NoError(t, err)
|
|
defer func() { _ = resp.Body.Close() }()
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
return resp, body
|
|
}
|
|
|
|
// ping calls GET /tests/ping with a valid app version.
|
|
func ping(t *testing.T, s *Server) (*http.Response, []byte) {
|
|
t.Helper()
|
|
return do(t, s, "GET", "/tests/ping", map[string]string{"x-pm-appversion": testAppVersion})
|
|
}
|
|
|
|
func decodeError(t *testing.T, body []byte) api.Error {
|
|
t.Helper()
|
|
var e api.Error
|
|
require.NoError(t, json.Unmarshal(body, &e), "body: %s", body)
|
|
return e
|
|
}
|
|
|
|
func TestLegacyRoutesGone(t *testing.T) {
|
|
s := New(t)
|
|
for _, path := range []string{
|
|
"/drive/shares/abc/links/def",
|
|
"/drive/shares/abc/folders/def/children",
|
|
"/drive/shares/abc/files",
|
|
"/drive/shares/abc/events/latest",
|
|
} {
|
|
for _, method := range []string{"GET", "POST"} {
|
|
resp, _ := do(t, s, method, path, map[string]string{"x-pm-appversion": testAppVersion})
|
|
assert.Equal(t, http.StatusGone, resp.StatusCode, "%s %s", method, path)
|
|
assert.Equal(t, 1, s.Requests()[method+" "+path], "%s %s counted", method, path)
|
|
}
|
|
}
|
|
|
|
// The share itself is not a legacy route.
|
|
resp, _ := do(t, s, "GET", "/drive/shares/abc", map[string]string{"x-pm-appversion": testAppVersion})
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
}
|
|
|
|
func TestLegacyRequestsProcessWide(t *testing.T) {
|
|
before := LegacyRequests()
|
|
s1, s2 := New(t), New(t)
|
|
do(t, s1, "GET", "/drive/shares/abc/links/def", map[string]string{"x-pm-appversion": testAppVersion})
|
|
do(t, s2, "GET", "/drive/shares/abc/links/def", map[string]string{"x-pm-appversion": testAppVersion})
|
|
do(t, s2, "POST", "/drive/shares/abc/files", nil)
|
|
do(t, s2, "GET", "/drive/shares/abc", nil)
|
|
s1.ResetRequests()
|
|
s2.ResetRequests()
|
|
|
|
after := LegacyRequests()
|
|
assert.Equal(t, before["GET /drive/shares/abc/links/def"]+2, after["GET /drive/shares/abc/links/def"])
|
|
assert.Equal(t, before["POST /drive/shares/abc/files"]+1, after["POST /drive/shares/abc/files"], "counted whatever its headers")
|
|
assert.NotContains(t, after, "GET /drive/shares/abc")
|
|
after["changed"] = 1
|
|
assert.NotContains(t, LegacyRequests(), "changed", "a copy is returned")
|
|
}
|
|
|
|
func TestUnknownRoute(t *testing.T) {
|
|
s := New(t)
|
|
resp, body := do(t, s, "GET", "/core/v4/nothing", map[string]string{"x-pm-appversion": testAppVersion})
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
|
|
assert.Contains(t, decodeError(t, body).Message, "GET /core/v4/nothing")
|
|
}
|
|
|
|
func TestAppVersionEnforced(t *testing.T) {
|
|
s := New(t)
|
|
for _, version := range []string{
|
|
"",
|
|
"web-drive@5.2.0",
|
|
"external-drive-rclone@1.77.0-dev",
|
|
"external-drive-other@1.77.0-stable",
|
|
"external-drive-rclone@1.77-stable",
|
|
"external-drive-rclone@1.77.0-stable+",
|
|
} {
|
|
header := map[string]string{}
|
|
if version != "" {
|
|
header["x-pm-appversion"] = version
|
|
}
|
|
resp, body := do(t, s, "GET", "/tests/ping", header)
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "version %q", version)
|
|
assert.Equal(t, api.CodeInvalidSection, decodeError(t, body).Code, "version %q", version)
|
|
}
|
|
|
|
for _, version := range []string{
|
|
"external-drive-rclone@1.77.0-stable",
|
|
"external-drive-rclone@1.77.0-beta+abc",
|
|
"external-drive-rclone@1.77.0-alpha",
|
|
"external-drive-rclone@1.77.0",
|
|
"external-drive-rclone@1.77.0-alpha+abc123f.dirty",
|
|
} {
|
|
resp, body := do(t, s, "GET", "/tests/ping", map[string]string{"x-pm-appversion": version})
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "version %q", version)
|
|
assert.JSONEq(t, `{"Code":1000}`, string(body), "version %q", version)
|
|
}
|
|
|
|
resp, _ := do(t, s, "GET", "/tests/ping", map[string]string{
|
|
"x-pm-appversion": testAppVersion,
|
|
"x-pm-drive-sdk-version": "js@0.23.0",
|
|
})
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
}
|
|
|
|
func TestFaultInjection(t *testing.T) {
|
|
t.Run("Times", func(t *testing.T) {
|
|
s := New(t)
|
|
s.Inject(Fault{Pattern: "/tests/ping", Times: 2, Status: 429, RetryAfter: 1})
|
|
for i := range 2 {
|
|
resp, _ := ping(t, s)
|
|
assert.Equal(t, http.StatusTooManyRequests, resp.StatusCode, "call %d", i)
|
|
assert.Equal(t, "1", resp.Header.Get("Retry-After"), "call %d", i)
|
|
}
|
|
resp, _ := ping(t, s)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
assert.Empty(t, resp.Header.Get("Retry-After"))
|
|
})
|
|
|
|
t.Run("Skip", func(t *testing.T) {
|
|
s := New(t)
|
|
s.Inject(Fault{Pattern: "/tests/ping", Skip: 1, Times: 1, Status: 503, Code: 2028})
|
|
resp, _ := ping(t, s)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
resp, body := ping(t, s)
|
|
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
|
|
assert.Equal(t, api.CodeFeatureBlocked, decodeError(t, body).Code)
|
|
resp, _ = ping(t, s)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Forever", func(t *testing.T) {
|
|
s := New(t)
|
|
s.Inject(Fault{Pattern: "/tests/ping", Status: 500})
|
|
for range 5 {
|
|
resp, _ := ping(t, s)
|
|
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
|
}
|
|
})
|
|
|
|
t.Run("Method", func(t *testing.T) {
|
|
s := New(t)
|
|
s.Inject(Fault{Method: "POST", Pattern: "/tests/ping", Status: 500})
|
|
resp, _ := ping(t, s)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Drop", func(t *testing.T) {
|
|
s := New(t)
|
|
s.Inject(Fault{Pattern: "/tests/ping", Times: 1, Drop: true})
|
|
req, err := http.NewRequest("GET", s.URL()+"/tests/ping", nil)
|
|
require.NoError(t, err)
|
|
req.Header.Set("x-pm-appversion", testAppVersion)
|
|
resp, err := s.Client().Do(req)
|
|
if err == nil {
|
|
_ = resp.Body.Close()
|
|
}
|
|
require.Error(t, err)
|
|
resp2, _ := ping(t, s)
|
|
assert.Equal(t, http.StatusOK, resp2.StatusCode)
|
|
})
|
|
|
|
t.Run("Delay", func(t *testing.T) {
|
|
s := New(t)
|
|
s.Inject(Fault{Pattern: "/tests/ping", Times: 1, Delay: 100 * time.Millisecond})
|
|
start := time.Now()
|
|
resp, _ := ping(t, s)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
assert.GreaterOrEqual(t, time.Since(start), 100*time.Millisecond)
|
|
})
|
|
|
|
t.Run("Gather", func(t *testing.T) {
|
|
s := New(t)
|
|
s.Inject(Fault{Pattern: "/tests/ping", Times: 2, Gather: true})
|
|
req, err := http.NewRequest("GET", s.URL()+"/tests/ping", nil)
|
|
require.NoError(t, err)
|
|
req.Header.Set("x-pm-appversion", testAppVersion)
|
|
first := make(chan int)
|
|
go func() {
|
|
resp, err := s.Client().Do(req)
|
|
if err != nil {
|
|
first <- 0
|
|
return
|
|
}
|
|
_ = resp.Body.Close()
|
|
first <- resp.StatusCode
|
|
}()
|
|
select {
|
|
case <-first:
|
|
t.Fatal("first request answered before the second arrived")
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
resp, _ := ping(t, s)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
assert.Equal(t, http.StatusOK, <-first)
|
|
// Requests after Times are not held.
|
|
resp, _ = ping(t, s)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
})
|
|
|
|
t.Run("Hold", func(t *testing.T) {
|
|
s := New(t)
|
|
n := 0
|
|
s.handle("GET", "/tests/count", func(w http.ResponseWriter, r *http.Request) {
|
|
n++
|
|
writeJSON(w, http.StatusOK, map[string]int{"Code": api.CodeOK, "N": n})
|
|
})
|
|
release := make(chan struct{})
|
|
held := make(chan struct{}, 1)
|
|
s.Inject(Fault{Pattern: "/tests/count", Times: 1, Hold: release, Held: held})
|
|
first := make(chan []byte)
|
|
go func() {
|
|
_, body := do(t, s, "GET", "/tests/count", map[string]string{"x-pm-appversion": testAppVersion})
|
|
first <- body
|
|
}()
|
|
<-held
|
|
// The held request was handled on arrival; later ones pass.
|
|
_, body := do(t, s, "GET", "/tests/count", map[string]string{"x-pm-appversion": testAppVersion})
|
|
assert.JSONEq(t, `{"Code":1000,"N":2}`, string(body))
|
|
select {
|
|
case <-first:
|
|
t.Fatal("held request answered before release")
|
|
default:
|
|
}
|
|
close(release)
|
|
assert.JSONEq(t, `{"Code":1000,"N":1}`, string(<-first))
|
|
})
|
|
|
|
t.Run("AfterHandler", func(t *testing.T) {
|
|
s := New(t)
|
|
called := 0
|
|
s.handle("POST", "/tests/commit", func(w http.ResponseWriter, r *http.Request) {
|
|
called++
|
|
writeJSON(w, http.StatusOK, map[string]int{"Code": api.CodeOK})
|
|
})
|
|
s.Inject(Fault{Pattern: "/tests/commit", Times: 1, Status: 500, AfterHandler: true})
|
|
resp, body := do(t, s, "POST", "/tests/commit", map[string]string{"x-pm-appversion": testAppVersion})
|
|
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
|
|
assert.NotContains(t, string(body), `"Code":1000`)
|
|
assert.Equal(t, 1, called)
|
|
})
|
|
}
|
|
|
|
func TestRequestCounting(t *testing.T) {
|
|
s := New(t)
|
|
s.handle("GET", "/tests/volumes/{volumeID}/folders/{linkID}/children", func(w http.ResponseWriter, r *http.Request) {
|
|
writeJSON(w, http.StatusOK, map[string]any{"Code": api.CodeOK, "Volume": r.PathValue("volumeID")})
|
|
})
|
|
header := map[string]string{"x-pm-appversion": testAppVersion}
|
|
_, body := do(t, s, "GET", "/tests/volumes/v1/folders/l1/children", header)
|
|
assert.JSONEq(t, `{"Code":1000,"Volume":"v1"}`, string(body))
|
|
do(t, s, "GET", "/tests/volumes/v2/folders/l2/children", header)
|
|
ping(t, s)
|
|
|
|
assert.Equal(t, map[string]int{
|
|
"GET /tests/volumes/{volumeID}/folders/{linkID}/children": 2,
|
|
"GET /tests/ping": 1,
|
|
}, s.Requests())
|
|
|
|
// The returned map is a copy.
|
|
s.Requests()["GET /tests/ping"] = 99
|
|
assert.Equal(t, 1, s.Requests()["GET /tests/ping"])
|
|
|
|
s.ResetRequests()
|
|
assert.Empty(t, s.Requests())
|
|
ping(t, s)
|
|
assert.Equal(t, map[string]int{"GET /tests/ping": 1}, s.Requests())
|
|
}
|
|
|
|
func TestClockSkew(t *testing.T) {
|
|
s := New(t)
|
|
check := func(skew time.Duration) {
|
|
resp, _ := ping(t, s)
|
|
date, err := http.ParseTime(resp.Header.Get("Date"))
|
|
require.NoError(t, err)
|
|
assert.WithinDuration(t, time.Now().Add(skew), date, 5*time.Second)
|
|
}
|
|
check(0)
|
|
s.SetClockSkew(-2 * time.Hour)
|
|
check(-2 * time.Hour)
|
|
s.SetClockSkew(30 * time.Minute)
|
|
check(30 * time.Minute)
|
|
}
|
|
|
|
func TestErrorJSON(t *testing.T) {
|
|
e := api.Error{Status: 422, Code: api.CodeAlreadyExists, Message: "exists", Details: json.RawMessage(`{"ConflictLinkID":"x"}`)}
|
|
b, err := json.Marshal(&e)
|
|
require.NoError(t, err)
|
|
assert.JSONEq(t, `{"Code":2500,"Error":"exists","Details":{"ConflictLinkID":"x"}}`, string(b))
|
|
assert.True(t, strings.Contains(e.Error(), "exists") && strings.Contains(e.Error(), "2500") && strings.Contains(e.Error(), "422"), e.Error())
|
|
}
|
|
|
|
func TestNewStandalone(t *testing.T) {
|
|
s, err := NewStandalone("127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
assert.False(t, strings.HasSuffix(s.URL(), ":0"), "URL %q must carry the bound port", s.URL())
|
|
resp, _ := ping(t, s)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
|
|
s.Close()
|
|
_, err = s.Client().Get(s.URL() + "/tests/ping")
|
|
assert.Error(t, err, "server still answering after Close")
|
|
|
|
_, err = NewStandalone("not an address")
|
|
assert.Error(t, err)
|
|
}
|