Skip to content

Commit

Permalink
v12.2.0 (#411)
Browse files Browse the repository at this point in the history
* Deserialize additionalInfo in ARM error

* Allow a new authorizer to be created from a configuration file by specifying a resource instead of a base url.

This enables resource like KeyVault and Container Registry to use an authorizer configured from a configuration file.

* [WIP] Using the Context from the timeout if provided (#315)

* Using the timeout from the context if available

- Makes PollingDuration optional

* Renaming the registration start time

* Making PollingDuration not a pointer

* fixing a broken reference

* Add NewAuthorizerFromCli method which uses Azure CLI to obtain a token for the currently logged in user, for  local development scenarios. (#316)

* Adding User assigned identity support for the MSIConfig authorizor (#332)

* Adding ByteSlicePtr (#399)

* Adding a new `WithXML` method (#402)

* Add HTTP status code response helpers (#403)

Added IsHTTPStatus() and HasHTTPStatus() methods to autorest.Response

* adding a new preparer for `MERGE` used in the Storage API's (#406)

* New Preparer/Responder for `Unmarshalling Bytes` (#407)

* New Preparer: WithBytes

* New Responder: `ByUnmarshallingBytes`

* Reusing the bytes, rather than copying them

* Fixing the broken test / switching to read the bytes directly

* Support HTTP-Date in Retry-After header (#410)

RFC specifies Retry-After header can be integer value expressing seconds
or an HTTP-Date indicating when to try again.
Removed superfluous check for HTTP status code.

* v12.2.0
  • Loading branch information
jhendrixMSFT authored Jun 25, 2019
1 parent f29a2ec commit 09205e8
Show file tree
Hide file tree
Showing 12 changed files with 255 additions and 6 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# CHANGELOG

## v12.2.0

### New Features

- Added `autorest.WithXML`, `autorest.AsMerge`, `autorest.WithBytes` preparer decorators.
- Added `autorest.ByUnmarshallingBytes` response decorator.
- Added `Response.IsHTTPStatus` and `Response.HasHTTPStatus` helper methods for inspecting HTTP status code in `autorest.Response` types.

### Bug Fixes

- `autorest.DelayWithRetryAfter` now supports HTTP-Dates in the `Retry-After` header and is not limited to just 429 status codes.

## v12.1.0

### New Features
Expand Down
16 changes: 16 additions & 0 deletions autorest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ type Response struct {
*http.Response `json:"-"`
}

// IsHTTPStatus returns true if the returned HTTP status code matches the provided status code.
// If there was no response (i.e. the underlying http.Response is nil) the return value is false.
func (r Response) IsHTTPStatus(statusCode int) bool {
if r.Response == nil {
return false
}
return r.Response.StatusCode == statusCode
}

// HasHTTPStatus returns true if the returned HTTP status code matches one of the provided status codes.
// If there was no response (i.e. the underlying http.Response is nil) or not status codes are provided
// the return value is false.
func (r Response) HasHTTPStatus(statusCodes ...int) bool {
return ResponseHasStatusCode(r.Response, statusCodes...)
}

// LoggingInspector implements request and response inspectors that log the full request and
// response to a supplied log.
type LoggingInspector struct {
Expand Down
31 changes: 31 additions & 0 deletions autorest/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,37 @@ func TestCookies(t *testing.T) {
}
}

func TestResponseIsHTTPStatus(t *testing.T) {
r := Response{}
if r.IsHTTPStatus(http.StatusBadRequest) {
t.Fatal("autorest: expected false for nil response")
}
r.Response = &http.Response{StatusCode: http.StatusOK}
if r.IsHTTPStatus(http.StatusBadRequest) {
t.Fatal("autorest: expected false")
}
if !r.IsHTTPStatus(http.StatusOK) {
t.Fatal("autorest: expected true")
}
}

func TestResponseHasHTTPStatus(t *testing.T) {
r := Response{}
if r.HasHTTPStatus(http.StatusBadRequest, http.StatusInternalServerError) {
t.Fatal("autorest: expected false for nil response")
}
r.Response = &http.Response{StatusCode: http.StatusAccepted}
if r.HasHTTPStatus(http.StatusBadRequest, http.StatusInternalServerError) {
t.Fatal("autorest: expected false")
}
if !r.HasHTTPStatus(http.StatusOK, http.StatusCreated, http.StatusAccepted) {
t.Fatal("autorest: expected true")
}
if r.HasHTTPStatus() {
t.Fatal("autorest: expected false for no status codes")
}
}

func randomString(n int) string {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
r := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
Expand Down
13 changes: 13 additions & 0 deletions autorest/mocks/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,19 @@ func NewResponse() *http.Response {
return NewResponseWithContent("")
}

// NewResponseWithBytes instantiates a new response with the passed bytes as the body content.
func NewResponseWithBytes(input []byte) *http.Response {
return &http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "HTTP/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
Body: NewBodyWithBytes(input),
Request: NewRequest(),
}
}

// NewResponseWithContent instantiates a new response with the passed string as the body content.
func NewResponseWithContent(c string) *http.Response {
return &http.Response{
Expand Down
8 changes: 8 additions & 0 deletions autorest/mocks/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ func NewBody(s string) *Body {
return (&Body{s: s}).reset()
}

// NewBodyWithBytes creates a new instance of Body.
func NewBodyWithBytes(b []byte) *Body {
return &Body{
b: b,
isOpen: true,
}
}

// NewBodyClose creates a new instance of Body.
func NewBodyClose(s string) *Body {
return &Body{s: s}
Expand Down
45 changes: 45 additions & 0 deletions autorest/preparer.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package autorest
import (
"bytes"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
Expand Down Expand Up @@ -190,6 +191,9 @@ func AsGet() PrepareDecorator { return WithMethod("GET") }
// AsHead returns a PrepareDecorator that sets the HTTP method to HEAD.
func AsHead() PrepareDecorator { return WithMethod("HEAD") }

// AsMerge returns a PrepareDecorator that sets the HTTP method to MERGE.
func AsMerge() PrepareDecorator { return WithMethod("MERGE") }

// AsOptions returns a PrepareDecorator that sets the HTTP method to OPTIONS.
func AsOptions() PrepareDecorator { return WithMethod("OPTIONS") }

Expand Down Expand Up @@ -225,6 +229,25 @@ func WithBaseURL(baseURL string) PrepareDecorator {
}
}

// WithBytes returns a PrepareDecorator that takes a list of bytes
// which passes the bytes directly to the body
func WithBytes(input *[]byte) PrepareDecorator {
return func(p Preparer) Preparer {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err == nil {
if input == nil {
return r, fmt.Errorf("Input Bytes was nil")
}

r.ContentLength = int64(len(*input))
r.Body = ioutil.NopCloser(bytes.NewReader(*input))
}
return r, err
})
}
}

// WithCustomBaseURL returns a PrepareDecorator that replaces brace-enclosed keys within the
// request base URL (i.e., http.Request.URL) with the corresponding values from the passed map.
func WithCustomBaseURL(baseURL string, urlParameters map[string]interface{}) PrepareDecorator {
Expand Down Expand Up @@ -377,6 +400,28 @@ func WithJSON(v interface{}) PrepareDecorator {
}
}

// WithXML returns a PrepareDecorator that encodes the data passed as XML into the body of the
// request and sets the Content-Length header.
func WithXML(v interface{}) PrepareDecorator {
return func(p Preparer) Preparer {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err == nil {
b, err := xml.Marshal(v)
if err == nil {
// we have to tack on an XML header
withHeader := xml.Header + string(b)
bytesWithHeader := []byte(withHeader)

r.ContentLength = int64(len(bytesWithHeader))
r.Body = ioutil.NopCloser(bytes.NewReader(bytesWithHeader))
}
}
return r, err
})
}
}

// WithPath returns a PrepareDecorator that adds the supplied path to the request URL. If the path
// is absolute (that is, it begins with a "/"), it replaces the existing path.
func WithPath(path string) PrepareDecorator {
Expand Down
51 changes: 51 additions & 0 deletions autorest/preparer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,30 @@ func ExampleWithBaseURL_second() {
// Output: parse :: missing protocol scheme
}

// Create a request whose Body is a byte array
func TestWithBytes(t *testing.T) {
input := []byte{41, 82, 109}

r, err := Prepare(&http.Request{},
WithBytes(&input))
if err != nil {
t.Fatalf("ERROR: %v\n", err)
}

b, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Fatalf("ERROR: %v\n", err)
}

if len(b) != len(input) {
t.Fatalf("Expected the Body to contain %d bytes but got %d", len(input), len(b))
}

if !reflect.DeepEqual(b, input) {
t.Fatalf("Body doesn't contain the same bytes: %s (Expected %s)", b, input)
}
}

func ExampleWithCustomBaseURL() {
r, err := Prepare(&http.Request{},
WithCustomBaseURL("https://{account}.{service}.core.windows.net/",
Expand Down Expand Up @@ -238,6 +262,26 @@ func ExampleWithJSON() {
// Output: Request Body contains {"name":"Rob Pike","age":42}
}

// Create a request whose Body is the XML encoding of a structure
func ExampleWithXML() {
t := mocks.T{Name: "Rob Pike", Age: 42}

r, err := Prepare(&http.Request{},
WithXML(&t))
if err != nil {
fmt.Printf("ERROR: %v\n", err)
}

b, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Printf("ERROR: %v\n", err)
} else {
fmt.Printf("Request Body contains %s\n", string(b))
}
// Output: Request Body contains <?xml version="1.0" encoding="UTF-8"?>
// <T><Name>Rob Pike</Name><Age>42</Age></T>
}

// Create a request from a path with escaped parameters
func ExampleWithEscapedPathParameters() {
params := map[string]interface{}{
Expand Down Expand Up @@ -448,6 +492,13 @@ func TestAsHead(t *testing.T) {
}
}

func TestAsMerge(t *testing.T) {
r, _ := Prepare(mocks.NewRequest(), AsMerge())
if r.Method != "MERGE" {
t.Fatal("autorest: AsMerge failed to set HTTP method header to MERGE")
}
}

func TestAsOptions(t *testing.T) {
r, _ := Prepare(mocks.NewRequest(), AsOptions())
if r.Method != "OPTIONS" {
Expand Down
19 changes: 19 additions & 0 deletions autorest/responder.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,25 @@ func ByClosingIfError() RespondDecorator {
}
}

// ByUnmarshallingBytes returns a RespondDecorator that copies the Bytes returned in the
// response Body into the value pointed to by v.
func ByUnmarshallingBytes(v *[]byte) RespondDecorator {
return func(r Responder) Responder {
return ResponderFunc(func(resp *http.Response) error {
err := r.Respond(resp)
if err == nil {
bytes, errInner := ioutil.ReadAll(resp.Body)
if errInner != nil {
err = fmt.Errorf("Error occurred reading http.Response#Body - Error = '%v'", errInner)
} else {
*v = bytes
}
}
return err
})
}
}

// ByUnmarshallingJSON returns a RespondDecorator that decodes a JSON document returned in the
// response Body into the value pointed to by v.
func ByUnmarshallingJSON(v interface{}) RespondDecorator {
Expand Down
19 changes: 19 additions & 0 deletions autorest/responder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,25 @@ func ExampleWithErrorUnlessOK() {
// Output: GET of https://microsoft.com/a/b/c/ returned HTTP 200
}

func TestByUnmarshallingBytes(t *testing.T) {
expected := []byte("Lorem Ipsum Dolor")

// we'll create a fixed-sized array here, since that's the expectation
bytes := make([]byte, len(expected))

Respond(mocks.NewResponseWithBytes(expected),
ByUnmarshallingBytes(&bytes),
ByClosing())

if len(bytes) != len(expected) {
t.Fatalf("Expected Response to be %d bytes but got %d bytes", len(expected), len(bytes))
}

if !reflect.DeepEqual(expected, bytes) {
t.Fatalf("Expected Response to be %s but got %s", expected, bytes)
}
}

func ExampleByUnmarshallingJSON() {
c := `
{
Expand Down
18 changes: 13 additions & 5 deletions autorest/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,16 +248,24 @@ func DoRetryForStatusCodes(attempts int, backoff time.Duration, codes ...int) Se
}
}

// DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header in
// responses with status code 429
// DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header.
// The value of Retry-After can be either the number of seconds or a date in RFC1123 format.
// The function returns true after successfully waiting for the specified duration. If there is
// no Retry-After header or the wait is cancelled the return value is false.
func DelayWithRetryAfter(resp *http.Response, cancel <-chan struct{}) bool {
if resp == nil {
return false
}
retryAfter, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
if resp.StatusCode == http.StatusTooManyRequests && retryAfter > 0 {
var dur time.Duration
ra := resp.Header.Get("Retry-After")
if retryAfter, _ := strconv.Atoi(ra); retryAfter > 0 {
dur = time.Duration(retryAfter) * time.Second
} else if t, err := time.Parse(time.RFC1123, ra); err == nil {
dur = t.Sub(time.Now())
}
if dur > 0 {
select {
case <-time.After(time.Duration(retryAfter) * time.Second):
case <-time.After(dur):
return true
case <-cancel:
return false
Expand Down
27 changes: 27 additions & 0 deletions autorest/sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,33 @@ func TestDelayWithRetryAfterWithSuccess(t *testing.T) {
}
}

func TestDelayWithRetryAfterWithSuccessDateTime(t *testing.T) {
resumeAt := time.Now().Add(2 * time.Second).Round(time.Second)

client := mocks.NewSender()
resp := mocks.NewResponseWithStatus("503 Service temporarily unavailable", http.StatusServiceUnavailable)
mocks.SetResponseHeader(resp, "Retry-After", resumeAt.Format(time.RFC1123))
client.AppendResponse(resp)
client.AppendResponse(mocks.NewResponseWithStatus("200 OK", http.StatusOK))

r, _ := SendWithSender(client, mocks.NewRequest(),
DoRetryForStatusCodes(1, time.Duration(time.Second), http.StatusServiceUnavailable),
)

if time.Now().Before(resumeAt) {
t.Fatal("autorest: DelayWithRetryAfter failed stopped too soon")
}

Respond(r,
ByDiscardingBody(),
ByClosing())

if client.Attempts() != 2 {
t.Fatalf("autorest: Sender#DelayWithRetryAfter -- Got: StatusCode %v in %v attempts; Want: StatusCode 200 OK in 2 attempts -- ",
r.Status, client.Attempts()-1)
}
}

type temporaryError struct {
message string
}
Expand Down
2 changes: 1 addition & 1 deletion autorest/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
"runtime"
)

const number = "v12.1.0"
const number = "v12.2.0"

var (
userAgent = fmt.Sprintf("Go/%s (%s-%s) go-autorest/%s",
Expand Down

0 comments on commit 09205e8

Please sign in to comment.