Files
rclone/backend/protondrive/protontest/server_test.go
T

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)
}