Skip to content

Commit

Permalink
Merge branch 'master' into fix/bpmeta-log-levels
Browse files Browse the repository at this point in the history
  • Loading branch information
g-awmalik committed Jul 17, 2023
2 parents b327faf + 9b5aed3 commit ed6fffa
Show file tree
Hide file tree
Showing 5 changed files with 397 additions and 25 deletions.
163 changes: 163 additions & 0 deletions infra/blueprint-test/pkg/utils/asserthttp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package utils

import (
"io"
"net/http"
"strings"
"time"

"github.com/mitchellh/go-testing-interface"
)

// AssertHTTP provides a collection of HTTP asserts.
type AssertHTTP struct {
httpClient *http.Client
}

type assertOption func(*AssertHTTP)

// WithHTTPClient specifies an HTTP client for the AssertHTTP use.
func WithHTTPClient(c *http.Client) assertOption {
return func(ah *AssertHTTP) {
ah.httpClient = c
}
}

// NewAssertHTTP creates a new AssertHTTP with option overrides.
func NewAssertHTTP(opts ...assertOption) *AssertHTTP {
ah := &AssertHTTP{http.DefaultClient}
for _, opt := range opts {
opt(ah)
}
return ah
}

// AssertSuccessWithRetry runs httpRequest and retries on errors outside client control.
func (ah *AssertHTTP) AssertSuccessWithRetry(t testing.TB, r *http.Request) {
t.Helper()
Poll(t, ah.httpRequest(t, r), 3, 2*time.Second)
}

// AssertSuccess runs httpRequest without retry.
func (ah *AssertHTTP) AssertSuccess(t testing.TB, r *http.Request) {
t.Helper()
_, err := ah.httpRequest(t, r)()
if err != nil {
t.Fatal(err)
}
}

// AssertResponseWithRetry runs httpResponse and retries on errors outside client control.
func (ah *AssertHTTP) AssertResponseWithRetry(t testing.TB, r *http.Request, wantCode int, want ...string) {
t.Helper()
Poll(t, ah.httpResponse(t, r, wantCode, want...), 3, 2*time.Second)
}

// AssertResponse runs httpResponse without retry.
func (ah *AssertHTTP) AssertResponse(t testing.TB, r *http.Request, wantCode int, want ...string) {
t.Helper()
_, err := ah.httpResponse(t, r, wantCode, want...)()
if err != nil {
t.Fatal(err)
}
}

// httpRequest verifies the request is successful by HTTP status code.
func (ah *AssertHTTP) httpRequest(t testing.TB, r *http.Request) func() (bool, error) {
t.Helper()
return func() (bool, error) {
t.Logf("Sending HTTP Request %s %s", r.Method, r.URL.String())
got, err := ah.httpClient.Do(r)
if err != nil {
return false, err
}
// Keep trying until the result is success or the request responsibility.
if retry := httpRetryCondition(got.StatusCode); retry {
return true, nil
}
// Any HTTP success will work. For a specific status use AssertResponse.
if got.StatusCode < http.StatusOK || got.StatusCode >= http.StatusMultipleChoices {
t.Errorf("want 2xx, got %d", got.StatusCode)
}

return false, nil
}
}

// httpResponse verifies the requested response has the wanted status code and payload.
func (ah *AssertHTTP) httpResponse(t testing.TB, r *http.Request, wantCode int, want ...string) func() (bool, error) {
t.Helper()
return func() (bool, error) {
t.Logf("Sending HTTP Request %s %s", r.Method, r.URL.String())
got, err := ah.httpClient.Do(r)
if err != nil {
return false, err
}
defer got.Body.Close()

if got.StatusCode != wantCode {
t.Errorf("response code: got %d, want %d", got.StatusCode, wantCode)
// Unwanted status code may be a server-side error condition that will clear.
// Assume unwanted success is not going to change.
return httpRetryCondition(got.StatusCode), nil
}

b, err := io.ReadAll(got.Body)
if err != nil {
return true, err
}
out := string(b)

atLeastOneError := false
for _, fragment := range want {
if !strings.Contains(out, fragment) {
t.Errorf("response body: want contained %q", fragment)
atLeastOneError = true
}
}

// Only output received HTTP response body once.
if atLeastOneError {
t.Log("response output:")
t.Log(out)
}

return false, nil
}
}

// httpRetryCondition indicates retry should be attempted on HTTP 1xx, 401, 403, and 5xx errors.
// 401 and 403 are retried in case of lagging authorization configuration.
// On true return value a retry is preferred.
func httpRetryCondition(code int) bool {
switch {
case code >= http.StatusOK && code < http.StatusMultipleChoices:
return false
case code < http.StatusOK:
return false
case code >= http.StatusInternalServerError:
return true
case code == http.StatusUnauthorized || code == http.StatusForbidden:
return true
case code >= http.StatusBadRequest:
return false
}

return false
}
209 changes: 209 additions & 0 deletions infra/blueprint-test/pkg/utils/asserthttp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package utils_test

import (
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils"
)

func TestAssertSuccess(t *testing.T) {
t.Run("success", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Hello World")
}))
defer ts.Close()

r, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}

it := &inspectableT{t, nil}
ah := utils.NewAssertHTTP(utils.WithHTTPClient(ts.Client()))
ah.AssertSuccess(it, r)

if it.err != nil {
t.Errorf("wanted success, got %v", it.err)
}
})
t.Run("request error", func(t *testing.T) {
r, err := http.NewRequest(http.MethodGet, "/nope", nil)
if err != nil {
t.Fatal(err)
}
it := &inspectableT{t, nil}
utils.NewAssertHTTP().AssertSuccess(it, r)

if it.err == nil {
t.Error("wanted error, got success")
}
})
t.Run("response error", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Available", http.StatusServiceUnavailable)
}))
defer ts.Close()

r, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}

it := &inspectableT{t, nil}
ah := utils.NewAssertHTTP(utils.WithHTTPClient(ts.Client()))
ah.AssertSuccess(it, r)

if it.err != nil {
t.Errorf("wanted error, got %v", it.err)
}
})
}

func TestAssertResponse(t *testing.T) {
t.Run("success", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Hello World")
}))
defer ts.Close()

r, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}

it := &inspectableT{t, nil}
ah := utils.NewAssertHTTP(utils.WithHTTPClient(ts.Client()))
ah.AssertResponse(it, r, http.StatusOK)
if it.err != nil {
t.Errorf("wanted success, got %v", it.err)
}
})
t.Run("request error", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Available", http.StatusServiceUnavailable)
}))
defer ts.Close()

r, err := http.NewRequest(http.MethodGet, "/nope", nil)
if err != nil {
t.Fatal(err)
}

it := &inspectableT{t, nil}
ah := utils.NewAssertHTTP(utils.WithHTTPClient(ts.Client()))
ah.AssertResponse(it, r, http.StatusServiceUnavailable)

if it.err == nil {
t.Error("wanted error, got success")
}
})
t.Run("response error", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Not Available", http.StatusServiceUnavailable)
}))
defer ts.Close()

r, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}

it := &inspectableT{t, nil}
ah := utils.NewAssertHTTP(utils.WithHTTPClient(ts.Client()))
ah.AssertResponse(it, r, http.StatusServiceUnavailable)

if it.err != nil {
t.Errorf("wanted error, got %v", it.err)
}
})
t.Run("response contains", func(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World")
}))
defer ts.Close()

r, err := http.NewRequest(http.MethodGet, ts.URL, nil)
if err != nil {
t.Fatal(err)
}

t.Run("success", func(t *testing.T) {
it := &inspectableT{t, nil}
ah := utils.NewAssertHTTP(utils.WithHTTPClient(ts.Client()))
ah.AssertResponse(it, r, http.StatusOK, "Hello", "World")
if it.err != nil {
t.Errorf("wanted success, got %v", it.err)
}
})
t.Run("error", func(t *testing.T) {
it := &inspectableT{t, nil}
ah := utils.NewAssertHTTP(utils.WithHTTPClient(ts.Client()))
ah.AssertResponse(it, r, http.StatusOK, "Hello", "Moon")
if it.err == nil {
t.Error("wanted error, got success")
}
})
t.Run("error multiple", func(t *testing.T) {
it := &inspectableT{t, nil}
ah := utils.NewAssertHTTP(utils.WithHTTPClient(ts.Client()))
ah.AssertResponse(it, r, http.StatusOK, "Hello", "Moon", "People")
if it.err == nil {
t.Error("wanted error, got success")
return
}
if e := it.err.Error(); !strings.Contains(e, "Moon") || !strings.Contains(e, "People") {
t.Errorf("wanted multiple errors, got one: %v", it.err)
}
})
})
}

// inspectableT wraps testing.T, overriding testing behavior to make error cases retrievable.
type inspectableT struct {
*testing.T
err error
}

func (it *inspectableT) Error(args ...interface{}) {
it.addError(args...)
}

func (it *inspectableT) Errorf(format string, args ...interface{}) {
a := append([]interface{}{format}, args)
it.addError(a)
}

func (it *inspectableT) Fatal(args ...interface{}) {
it.addError(args...)
}

func (it *inspectableT) Fatalf(format string, args ...interface{}) {
a := append([]interface{}{format}, args)
it.addError(a)
}

func (it *inspectableT) addError(args ...interface{}) {
s := fmt.Sprint(args...)
it.err = errors.Join(it.err, errors.New(s))
}
Loading

0 comments on commit ed6fffa

Please sign in to comment.