77e69b9cf3
Signed-off-by: Olivier Gambier <olivier@docker.com>
599 lines
15 KiB
Go
599 lines
15 KiB
Go
// Copyright 2011 Google Inc. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package googleapi
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
type SetOpaqueTest struct {
|
|
in *url.URL
|
|
wantRequestURI string
|
|
}
|
|
|
|
var setOpaqueTests = []SetOpaqueTest{
|
|
// no path
|
|
{
|
|
&url.URL{
|
|
Scheme: "http",
|
|
Host: "www.golang.org",
|
|
},
|
|
"http://www.golang.org",
|
|
},
|
|
// path
|
|
{
|
|
&url.URL{
|
|
Scheme: "http",
|
|
Host: "www.golang.org",
|
|
Path: "/",
|
|
},
|
|
"http://www.golang.org/",
|
|
},
|
|
// file with hex escaping
|
|
{
|
|
&url.URL{
|
|
Scheme: "https",
|
|
Host: "www.golang.org",
|
|
Path: "/file%20one&two",
|
|
},
|
|
"https://www.golang.org/file%20one&two",
|
|
},
|
|
// query
|
|
{
|
|
&url.URL{
|
|
Scheme: "http",
|
|
Host: "www.golang.org",
|
|
Path: "/",
|
|
RawQuery: "q=go+language",
|
|
},
|
|
"http://www.golang.org/?q=go+language",
|
|
},
|
|
// file with hex escaping in path plus query
|
|
{
|
|
&url.URL{
|
|
Scheme: "https",
|
|
Host: "www.golang.org",
|
|
Path: "/file%20one&two",
|
|
RawQuery: "q=go+language",
|
|
},
|
|
"https://www.golang.org/file%20one&two?q=go+language",
|
|
},
|
|
// query with hex escaping
|
|
{
|
|
&url.URL{
|
|
Scheme: "http",
|
|
Host: "www.golang.org",
|
|
Path: "/",
|
|
RawQuery: "q=go%20language",
|
|
},
|
|
"http://www.golang.org/?q=go%20language",
|
|
},
|
|
}
|
|
|
|
// prefixTmpl is a template for the expected prefix of the output of writing
|
|
// an HTTP request.
|
|
const prefixTmpl = "GET %v HTTP/1.1\r\nHost: %v\r\n"
|
|
|
|
func TestSetOpaque(t *testing.T) {
|
|
for _, test := range setOpaqueTests {
|
|
u := *test.in
|
|
SetOpaque(&u)
|
|
|
|
w := &bytes.Buffer{}
|
|
r := &http.Request{URL: &u}
|
|
if err := r.Write(w); err != nil {
|
|
t.Errorf("write request: %v", err)
|
|
continue
|
|
}
|
|
|
|
prefix := fmt.Sprintf(prefixTmpl, test.wantRequestURI, test.in.Host)
|
|
if got := string(w.Bytes()); !strings.HasPrefix(got, prefix) {
|
|
t.Errorf("got %q expected prefix %q", got, prefix)
|
|
}
|
|
}
|
|
}
|
|
|
|
type ExpandTest struct {
|
|
in string
|
|
expansions map[string]string
|
|
want string
|
|
}
|
|
|
|
var expandTests = []ExpandTest{
|
|
// no expansions
|
|
{
|
|
"http://www.golang.org/",
|
|
map[string]string{},
|
|
"http://www.golang.org/",
|
|
},
|
|
// one expansion, no escaping
|
|
{
|
|
"http://www.golang.org/{bucket}/delete",
|
|
map[string]string{
|
|
"bucket": "red",
|
|
},
|
|
"http://www.golang.org/red/delete",
|
|
},
|
|
// one expansion, with hex escapes
|
|
{
|
|
"http://www.golang.org/{bucket}/delete",
|
|
map[string]string{
|
|
"bucket": "red/blue",
|
|
},
|
|
"http://www.golang.org/red%2Fblue/delete",
|
|
},
|
|
// one expansion, with space
|
|
{
|
|
"http://www.golang.org/{bucket}/delete",
|
|
map[string]string{
|
|
"bucket": "red or blue",
|
|
},
|
|
"http://www.golang.org/red%20or%20blue/delete",
|
|
},
|
|
// expansion not found
|
|
{
|
|
"http://www.golang.org/{object}/delete",
|
|
map[string]string{
|
|
"bucket": "red or blue",
|
|
},
|
|
"http://www.golang.org//delete",
|
|
},
|
|
// multiple expansions
|
|
{
|
|
"http://www.golang.org/{one}/{two}/{three}/get",
|
|
map[string]string{
|
|
"one": "ONE",
|
|
"two": "TWO",
|
|
"three": "THREE",
|
|
},
|
|
"http://www.golang.org/ONE/TWO/THREE/get",
|
|
},
|
|
// utf-8 characters
|
|
{
|
|
"http://www.golang.org/{bucket}/get",
|
|
map[string]string{
|
|
"bucket": "£100",
|
|
},
|
|
"http://www.golang.org/%C2%A3100/get",
|
|
},
|
|
// punctuations
|
|
{
|
|
"http://www.golang.org/{bucket}/get",
|
|
map[string]string{
|
|
"bucket": `/\@:,.`,
|
|
},
|
|
"http://www.golang.org/%2F%5C%40%3A%2C./get",
|
|
},
|
|
// mis-matched brackets
|
|
{
|
|
"http://www.golang.org/{bucket/get",
|
|
map[string]string{
|
|
"bucket": "red",
|
|
},
|
|
"http://www.golang.org/{bucket/get",
|
|
},
|
|
// "+" prefix for suppressing escape
|
|
// See also: http://tools.ietf.org/html/rfc6570#section-3.2.3
|
|
{
|
|
"http://www.golang.org/{+topic}",
|
|
map[string]string{
|
|
"topic": "/topics/myproject/mytopic",
|
|
},
|
|
// The double slashes here look weird, but it's intentional
|
|
"http://www.golang.org//topics/myproject/mytopic",
|
|
},
|
|
}
|
|
|
|
func TestExpand(t *testing.T) {
|
|
for i, test := range expandTests {
|
|
u := url.URL{
|
|
Path: test.in,
|
|
}
|
|
Expand(&u, test.expansions)
|
|
got := u.Path
|
|
if got != test.want {
|
|
t.Errorf("got %q expected %q in test %d", got, test.want, i+1)
|
|
}
|
|
}
|
|
}
|
|
|
|
type CheckResponseTest struct {
|
|
in *http.Response
|
|
bodyText string
|
|
want error
|
|
errText string
|
|
}
|
|
|
|
var checkResponseTests = []CheckResponseTest{
|
|
{
|
|
&http.Response{
|
|
StatusCode: http.StatusOK,
|
|
},
|
|
"",
|
|
nil,
|
|
"",
|
|
},
|
|
{
|
|
&http.Response{
|
|
StatusCode: http.StatusInternalServerError,
|
|
},
|
|
`{"error":{}}`,
|
|
&Error{
|
|
Code: http.StatusInternalServerError,
|
|
Body: `{"error":{}}`,
|
|
},
|
|
`googleapi: got HTTP response code 500 with body: {"error":{}}`,
|
|
},
|
|
{
|
|
&http.Response{
|
|
StatusCode: http.StatusNotFound,
|
|
},
|
|
`{"error":{"message":"Error message for StatusNotFound."}}`,
|
|
&Error{
|
|
Code: http.StatusNotFound,
|
|
Message: "Error message for StatusNotFound.",
|
|
Body: `{"error":{"message":"Error message for StatusNotFound."}}`,
|
|
},
|
|
"googleapi: Error 404: Error message for StatusNotFound.",
|
|
},
|
|
{
|
|
&http.Response{
|
|
StatusCode: http.StatusBadRequest,
|
|
},
|
|
`{"error":"invalid_token","error_description":"Invalid Value"}`,
|
|
&Error{
|
|
Code: http.StatusBadRequest,
|
|
Body: `{"error":"invalid_token","error_description":"Invalid Value"}`,
|
|
},
|
|
`googleapi: got HTTP response code 400 with body: {"error":"invalid_token","error_description":"Invalid Value"}`,
|
|
},
|
|
{
|
|
&http.Response{
|
|
StatusCode: http.StatusBadRequest,
|
|
},
|
|
`{"error":{"errors":[{"domain":"usageLimits","reason":"keyInvalid","message":"Bad Request"}],"code":400,"message":"Bad Request"}}`,
|
|
&Error{
|
|
Code: http.StatusBadRequest,
|
|
Errors: []ErrorItem{
|
|
{
|
|
Reason: "keyInvalid",
|
|
Message: "Bad Request",
|
|
},
|
|
},
|
|
Body: `{"error":{"errors":[{"domain":"usageLimits","reason":"keyInvalid","message":"Bad Request"}],"code":400,"message":"Bad Request"}}`,
|
|
Message: "Bad Request",
|
|
},
|
|
"googleapi: Error 400: Bad Request, keyInvalid",
|
|
},
|
|
}
|
|
|
|
func TestCheckResponse(t *testing.T) {
|
|
for _, test := range checkResponseTests {
|
|
res := test.in
|
|
if test.bodyText != "" {
|
|
res.Body = ioutil.NopCloser(strings.NewReader(test.bodyText))
|
|
}
|
|
g := CheckResponse(res)
|
|
if !reflect.DeepEqual(g, test.want) {
|
|
t.Errorf("CheckResponse: got %v, want %v", g, test.want)
|
|
gotJson, err := json.Marshal(g)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
wantJson, err := json.Marshal(test.want)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
t.Errorf("json(got): %q\njson(want): %q", string(gotJson), string(wantJson))
|
|
}
|
|
if g != nil && g.Error() != test.errText {
|
|
t.Errorf("CheckResponse: unexpected error message.\nGot: %q\nwant: %q", g, test.errText)
|
|
}
|
|
}
|
|
}
|
|
|
|
type VariantPoint struct {
|
|
Type string
|
|
Coordinates []float64
|
|
}
|
|
|
|
type VariantTest struct {
|
|
in map[string]interface{}
|
|
result bool
|
|
want VariantPoint
|
|
}
|
|
|
|
var coords = []interface{}{1.0, 2.0}
|
|
|
|
var variantTests = []VariantTest{
|
|
{
|
|
in: map[string]interface{}{
|
|
"type": "Point",
|
|
"coordinates": coords,
|
|
},
|
|
result: true,
|
|
want: VariantPoint{
|
|
Type: "Point",
|
|
Coordinates: []float64{1.0, 2.0},
|
|
},
|
|
},
|
|
{
|
|
in: map[string]interface{}{
|
|
"type": "Point",
|
|
"bogus": coords,
|
|
},
|
|
result: true,
|
|
want: VariantPoint{
|
|
Type: "Point",
|
|
},
|
|
},
|
|
}
|
|
|
|
func TestVariantType(t *testing.T) {
|
|
for _, test := range variantTests {
|
|
if g := VariantType(test.in); g != test.want.Type {
|
|
t.Errorf("VariantType(%v): got %v, want %v", test.in, g, test.want.Type)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestConvertVariant(t *testing.T) {
|
|
for _, test := range variantTests {
|
|
g := VariantPoint{}
|
|
r := ConvertVariant(test.in, &g)
|
|
if r != test.result {
|
|
t.Errorf("ConvertVariant(%v): got %v, want %v", test.in, r, test.result)
|
|
}
|
|
if !reflect.DeepEqual(g, test.want) {
|
|
t.Errorf("ConvertVariant(%v): got %v, want %v", test.in, g, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
type unexpectedReader struct{}
|
|
|
|
func (unexpectedReader) Read([]byte) (int, error) {
|
|
return 0, fmt.Errorf("unexpected read in test.")
|
|
}
|
|
|
|
var contentRangeRE = regexp.MustCompile(`^bytes (\d+)\-(\d+)/(\d+)$`)
|
|
|
|
func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
t.req = req
|
|
if rng := req.Header.Get("Content-Range"); rng != "" && !strings.HasPrefix(rng, "bytes */") { // Read the data
|
|
m := contentRangeRE.FindStringSubmatch(rng)
|
|
if len(m) != 4 {
|
|
return nil, fmt.Errorf("unable to parse content range: %v", rng)
|
|
}
|
|
start, err := strconv.ParseInt(m[1], 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse content range: %v", rng)
|
|
}
|
|
end, err := strconv.ParseInt(m[2], 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse content range: %v", rng)
|
|
}
|
|
totalSize, err := strconv.ParseInt(m[3], 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse content range: %v", rng)
|
|
}
|
|
partialSize := end - start + 1
|
|
t.buf, err = ioutil.ReadAll(req.Body)
|
|
if err != nil || int64(len(t.buf)) != partialSize {
|
|
return nil, fmt.Errorf("unable to read %v bytes from request data, n=%v: %v", partialSize, len(t.buf), err)
|
|
}
|
|
if totalSize == end+1 {
|
|
t.statusCode = 200 // signify completion of transfer
|
|
}
|
|
}
|
|
f := ioutil.NopCloser(unexpectedReader{})
|
|
res := &http.Response{
|
|
Body: f,
|
|
StatusCode: t.statusCode,
|
|
Header: http.Header{},
|
|
}
|
|
if t.rangeVal != "" {
|
|
res.Header.Set("Range", t.rangeVal)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
type testTransport struct {
|
|
req *http.Request
|
|
statusCode int
|
|
rangeVal string
|
|
want int64
|
|
buf []byte
|
|
}
|
|
|
|
var statusTests = []*testTransport{
|
|
&testTransport{statusCode: 308, want: 0},
|
|
&testTransport{statusCode: 308, rangeVal: "bytes=0-0", want: 1},
|
|
&testTransport{statusCode: 308, rangeVal: "bytes=0-42", want: 43},
|
|
}
|
|
|
|
func TestTransferStatus(t *testing.T) {
|
|
ctx := context.Background()
|
|
for _, tr := range statusTests {
|
|
rx := &ResumableUpload{
|
|
Client: &http.Client{Transport: tr},
|
|
}
|
|
g, _, err := rx.transferStatus(ctx)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
if g != tr.want {
|
|
t.Errorf("transferStatus got %v, want %v", g, tr.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *interruptedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
t.req = req
|
|
if rng := req.Header.Get("Content-Range"); rng != "" && !strings.HasPrefix(rng, "bytes */") {
|
|
t.interruptCount += 1
|
|
if t.interruptCount%7 == 0 { // Respond with a "service unavailable" error
|
|
res := &http.Response{
|
|
StatusCode: http.StatusServiceUnavailable,
|
|
Header: http.Header{},
|
|
}
|
|
t.rangeVal = fmt.Sprintf("bytes=0-%v", len(t.buf)-1) // Set the response for next time
|
|
return res, nil
|
|
}
|
|
m := contentRangeRE.FindStringSubmatch(rng)
|
|
if len(m) != 4 {
|
|
return nil, fmt.Errorf("unable to parse content range: %v", rng)
|
|
}
|
|
start, err := strconv.ParseInt(m[1], 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse content range: %v", rng)
|
|
}
|
|
end, err := strconv.ParseInt(m[2], 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse content range: %v", rng)
|
|
}
|
|
totalSize, err := strconv.ParseInt(m[3], 10, 64)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse content range: %v", rng)
|
|
}
|
|
partialSize := end - start + 1
|
|
buf, err := ioutil.ReadAll(req.Body)
|
|
if err != nil || int64(len(buf)) != partialSize {
|
|
return nil, fmt.Errorf("unable to read %v bytes from request data, n=%v: %v", partialSize, len(buf), err)
|
|
}
|
|
t.buf = append(t.buf, buf...)
|
|
if totalSize == end+1 {
|
|
t.statusCode = 200 // signify completion of transfer
|
|
}
|
|
}
|
|
f := ioutil.NopCloser(unexpectedReader{})
|
|
res := &http.Response{
|
|
Body: f,
|
|
StatusCode: t.statusCode,
|
|
Header: http.Header{},
|
|
}
|
|
if t.rangeVal != "" {
|
|
res.Header.Set("Range", t.rangeVal)
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
type interruptedTransport struct {
|
|
req *http.Request
|
|
statusCode int
|
|
rangeVal string
|
|
interruptCount int
|
|
buf []byte
|
|
progressBuf string
|
|
}
|
|
|
|
func (tr *interruptedTransport) ProgressUpdate(current, total int64) {
|
|
tr.progressBuf += fmt.Sprintf("%v, %v\n", current, total)
|
|
}
|
|
|
|
func TestInterruptedTransferChunks(t *testing.T) {
|
|
f, err := os.Open("googleapi.go")
|
|
if err != nil {
|
|
t.Fatalf("unable to open googleapi.go: %v", err)
|
|
}
|
|
defer f.Close()
|
|
slurp, err := ioutil.ReadAll(f)
|
|
if err != nil {
|
|
t.Fatalf("unable to slurp file: %v", err)
|
|
}
|
|
st, err := f.Stat()
|
|
if err != nil {
|
|
t.Fatalf("unable to stat googleapi.go: %v", err)
|
|
}
|
|
tr := &interruptedTransport{
|
|
statusCode: 308,
|
|
buf: make([]byte, 0, st.Size()),
|
|
}
|
|
oldChunkSize := chunkSize
|
|
defer func() { chunkSize = oldChunkSize }()
|
|
chunkSize = 100 // override to process small chunks for test.
|
|
|
|
sleep = func(time.Duration) {} // override time.Sleep
|
|
rx := &ResumableUpload{
|
|
Client: &http.Client{Transport: tr},
|
|
Media: f,
|
|
MediaType: "text/plain",
|
|
ContentLength: st.Size(),
|
|
Callback: tr.ProgressUpdate,
|
|
}
|
|
res, err := rx.Upload(context.Background())
|
|
if err != nil || res == nil || res.StatusCode != http.StatusOK {
|
|
if res == nil {
|
|
t.Errorf("transferChunks not successful, res=nil: %v", err)
|
|
} else {
|
|
t.Errorf("transferChunks not successful, statusCode=%v: %v", res.StatusCode, err)
|
|
}
|
|
}
|
|
if len(tr.buf) != len(slurp) || bytes.Compare(tr.buf, slurp) != 0 {
|
|
t.Errorf("transferred file corrupted:\ngot %s\nwant %s", tr.buf, slurp)
|
|
}
|
|
w := ""
|
|
for i := chunkSize; i <= st.Size(); i += chunkSize {
|
|
w += fmt.Sprintf("%v, %v\n", i, st.Size())
|
|
}
|
|
if st.Size()%chunkSize != 0 {
|
|
w += fmt.Sprintf("%v, %v\n", st.Size(), st.Size())
|
|
}
|
|
if tr.progressBuf != w {
|
|
t.Errorf("progress update error, got %v, want %v", tr.progressBuf, w)
|
|
}
|
|
}
|
|
|
|
func TestCancelUpload(t *testing.T) {
|
|
f, err := os.Open("googleapi.go")
|
|
if err != nil {
|
|
t.Fatalf("unable to open googleapi.go: %v", err)
|
|
}
|
|
defer f.Close()
|
|
st, err := f.Stat()
|
|
if err != nil {
|
|
t.Fatalf("unable to stat googleapi.go: %v", err)
|
|
}
|
|
tr := &interruptedTransport{
|
|
statusCode: 308,
|
|
buf: make([]byte, 0, st.Size()),
|
|
}
|
|
oldChunkSize := chunkSize
|
|
defer func() { chunkSize = oldChunkSize }()
|
|
chunkSize = 100 // override to process small chunks for test.
|
|
|
|
sleep = func(time.Duration) {} // override time.Sleep
|
|
rx := &ResumableUpload{
|
|
Client: &http.Client{Transport: tr},
|
|
Media: f,
|
|
MediaType: "text/plain",
|
|
ContentLength: st.Size(),
|
|
Callback: tr.ProgressUpdate,
|
|
}
|
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
|
cancelFunc() // stop the upload that hasn't started yet
|
|
res, err := rx.Upload(ctx)
|
|
if err == nil || res == nil || res.StatusCode != http.StatusRequestTimeout {
|
|
if res == nil {
|
|
t.Errorf("transferChunks not successful, got res=nil, err=%v, want StatusRequestTimeout", err)
|
|
} else {
|
|
t.Errorf("transferChunks not successful, got statusCode=%v, err=%v, want StatusRequestTimeout", res.StatusCode, err)
|
|
}
|
|
}
|
|
}
|