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