Add debug info while issuing request (#50)
* Add debug info while issuing request * wip * wip * Rework tests
This commit is contained in:
+196
-195
@@ -2,8 +2,10 @@ package scaleset
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -11,129 +13,14 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestActionsError(t *testing.T) {
|
||||
t.Run("contains the status code, activity ID, and error", func(t *testing.T) {
|
||||
err := &ActionsError{
|
||||
ActivityID: "activity-id",
|
||||
StatusCode: 404,
|
||||
Err: errors.New("example error description"),
|
||||
}
|
||||
type readErrCloser struct{}
|
||||
|
||||
s := err.Error()
|
||||
assert.Contains(t, s, "StatusCode 404")
|
||||
assert.Contains(t, s, "ActivityId \"activity-id\"")
|
||||
assert.Contains(t, s, "example error description")
|
||||
})
|
||||
|
||||
t.Run("unwraps the error", func(t *testing.T) {
|
||||
err := &ActionsError{
|
||||
ActivityID: "activity-id",
|
||||
StatusCode: 404,
|
||||
Err: &actionsExceptionError{
|
||||
ExceptionName: "exception-name",
|
||||
Message: "example error message",
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, err.Unwrap(), err.Err)
|
||||
})
|
||||
|
||||
t.Run("is exception is ok", func(t *testing.T) {
|
||||
err := &ActionsError{
|
||||
ActivityID: "activity-id",
|
||||
StatusCode: 404,
|
||||
Err: &actionsExceptionError{
|
||||
ExceptionName: "exception-name",
|
||||
Message: "example error message",
|
||||
},
|
||||
}
|
||||
|
||||
var exception *actionsExceptionError
|
||||
assert.True(t, errors.As(err, &exception))
|
||||
|
||||
assert.True(t, err.isException("exception-name"))
|
||||
})
|
||||
|
||||
t.Run("is exception is not ok", func(t *testing.T) {
|
||||
tt := map[string]*ActionsError{
|
||||
"not an exception": {
|
||||
ActivityID: "activity-id",
|
||||
StatusCode: 404,
|
||||
Err: errors.New("example error description"),
|
||||
},
|
||||
"not target exception": {
|
||||
ActivityID: "activity-id",
|
||||
StatusCode: 404,
|
||||
Err: &actionsExceptionError{
|
||||
ExceptionName: "exception-name",
|
||||
Message: "example error message",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
targetException := "target-exception"
|
||||
for name, err := range tt {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.False(t, err.isException(targetException))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("is agent exists exception", func(t *testing.T) {
|
||||
err := &ActionsError{
|
||||
ActivityID: "activity-id",
|
||||
StatusCode: 404,
|
||||
Err: &actionsExceptionError{
|
||||
ExceptionName: "AgentExistsException",
|
||||
Message: "example error message",
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, err.IsAgentExists())
|
||||
})
|
||||
|
||||
t.Run("is agent not found exception", func(t *testing.T) {
|
||||
err := &ActionsError{
|
||||
ActivityID: "activity-id",
|
||||
StatusCode: 404,
|
||||
Err: &actionsExceptionError{
|
||||
ExceptionName: "AgentNotFoundException",
|
||||
Message: "example error message",
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, err.IsAgentNotFound())
|
||||
})
|
||||
|
||||
t.Run("is job still running exception", func(t *testing.T) {
|
||||
err := &ActionsError{
|
||||
ActivityID: "activity-id",
|
||||
StatusCode: 404,
|
||||
Err: &actionsExceptionError{
|
||||
ExceptionName: "JobStillRunningException",
|
||||
Message: "example error message",
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, err.IsJobStillRunning())
|
||||
})
|
||||
|
||||
t.Run("is message queue token expired exception", func(t *testing.T) {
|
||||
err := &ActionsError{
|
||||
ActivityID: "activity-id",
|
||||
StatusCode: 401,
|
||||
Err: &messageQueueTokenExpiredError{
|
||||
message: "example error message",
|
||||
},
|
||||
}
|
||||
|
||||
assert.True(t, err.IsMessageQueueTokenExpired())
|
||||
})
|
||||
}
|
||||
func (readErrCloser) Read([]byte) (int, error) { return 0, fmt.Errorf("read failed") }
|
||||
func (readErrCloser) Close() error { return nil }
|
||||
|
||||
func TestActionsExceptionError(t *testing.T) {
|
||||
t.Run("contains the exception name and message", func(t *testing.T) {
|
||||
err := &actionsExceptionError{
|
||||
err := actionsExceptionError{
|
||||
ExceptionName: "exception-name",
|
||||
Message: "example error message",
|
||||
}
|
||||
@@ -144,109 +31,223 @@ func TestActionsExceptionError(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitHubAPIError(t *testing.T) {
|
||||
t.Run("contains the status code, request ID, and error", func(t *testing.T) {
|
||||
err := &GitHubAPIError{
|
||||
StatusCode: 404,
|
||||
RequestID: "request-id",
|
||||
Err: errors.New("example error description"),
|
||||
}
|
||||
func TestNewRequestResponseError(t *testing.T) {
|
||||
req := func(t *testing.T) *http.Request {
|
||||
t.Helper()
|
||||
u, err := url.Parse("https://example.com/org/repo")
|
||||
require.NoError(t, err)
|
||||
return &http.Request{Method: http.MethodGet, URL: u}
|
||||
}
|
||||
|
||||
s := err.Error()
|
||||
assert.Contains(t, s, "StatusCode 404")
|
||||
assert.Contains(t, s, "RequestID \"request-id\"")
|
||||
assert.Contains(t, s, "example error description")
|
||||
t.Run("resp is nil", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
err := newRequestResponseError(req(t), nil, base)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "request GET https://example.com/org/repo failed")
|
||||
assert.True(t, errors.Is(err, base))
|
||||
})
|
||||
|
||||
t.Run("unwraps the error", func(t *testing.T) {
|
||||
err := &GitHubAPIError{
|
||||
StatusCode: 404,
|
||||
RequestID: "request-id",
|
||||
Err: errors.New("example error description"),
|
||||
t.Run("resp body is nil", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
resp := &http.Response{
|
||||
Status: "500 Internal Server Error",
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
ContentLength: 123,
|
||||
Header: make(http.Header),
|
||||
Body: nil,
|
||||
}
|
||||
|
||||
assert.Equal(t, err.Unwrap(), err.Err)
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unknown error")
|
||||
assert.True(t, errors.Is(err, base))
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseActionsErrorFromResponse(t *testing.T) {
|
||||
t.Run("empty content length", func(t *testing.T) {
|
||||
response := &http.Response{
|
||||
t.Run("empty body returns unknown error", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
resp := &http.Response{
|
||||
Status: "404 Not Found",
|
||||
StatusCode: http.StatusNotFound,
|
||||
ContentLength: 0,
|
||||
Header: http.Header{},
|
||||
StatusCode: 404,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
response.Header.Add(headerActionsActivityID, "activity-id")
|
||||
resp.Header.Set(headerActionsActivityID, "activity-id")
|
||||
resp.Header.Set(headerGitHubRequestID, "request-id")
|
||||
|
||||
err := ParseActionsErrorFromResponse(response)
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, "activity-id", err.(*ActionsError).ActivityID)
|
||||
assert.Equal(t, 404, err.(*ActionsError).StatusCode)
|
||||
assert.Equal(t, "unknown exception", err.(*ActionsError).Err.Error())
|
||||
assert.Contains(t, err.Error(), "status=\"404 Not Found\"")
|
||||
assert.Contains(t, err.Error(), "activity_id=\"activity-id\"")
|
||||
assert.Contains(t, err.Error(), "github_request_id=\"request-id\"")
|
||||
assert.Contains(t, err.Error(), "unknown error")
|
||||
assert.True(t, errors.Is(err, base))
|
||||
})
|
||||
|
||||
t.Run("contains text plain error", func(t *testing.T) {
|
||||
errorMessage := "example error message"
|
||||
response := &http.Response{
|
||||
ContentLength: int64(len(errorMessage)),
|
||||
StatusCode: 404,
|
||||
Header: http.Header{},
|
||||
Body: io.NopCloser(strings.NewReader(errorMessage)),
|
||||
t.Run("read body failure includes read error", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
resp := &http.Response{
|
||||
Status: "400 Bad Request",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
ContentLength: 1,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(readErrCloser{}),
|
||||
}
|
||||
response.Header.Add(headerActionsActivityID, "activity-id")
|
||||
response.Header.Add("Content-Type", "text/plain")
|
||||
|
||||
err := ParseActionsErrorFromResponse(response)
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
var actionsError *ActionsError
|
||||
assert.ErrorAs(t, err, &actionsError)
|
||||
assert.Equal(t, "activity-id", actionsError.ActivityID)
|
||||
assert.Equal(t, 404, actionsError.StatusCode)
|
||||
assert.Equal(t, errorMessage, actionsError.Err.Error())
|
||||
assert.Contains(t, err.Error(), "failed to read error response body")
|
||||
assert.True(t, errors.Is(err, base))
|
||||
assert.Contains(t, err.Error(), "read failed")
|
||||
})
|
||||
|
||||
t.Run("contains json error", func(t *testing.T) {
|
||||
errorMessage := `{"typeName":"exception-name","message":"example error message"}`
|
||||
response := &http.Response{
|
||||
ContentLength: int64(len(errorMessage)),
|
||||
StatusCode: 404,
|
||||
Header: http.Header{},
|
||||
Body: io.NopCloser(strings.NewReader(errorMessage)),
|
||||
t.Run("unknown content length and empty body returns unknown error", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
resp := &http.Response{
|
||||
Status: "400 Bad Request",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
ContentLength: -1,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
}
|
||||
response.Header.Add(headerActionsActivityID, "activity-id")
|
||||
response.Header.Add("Content-Type", "application/json")
|
||||
|
||||
err := ParseActionsErrorFromResponse(response)
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
var actionsError *ActionsError
|
||||
assert.ErrorAs(t, err, &actionsError)
|
||||
assert.Equal(t, "activity-id", actionsError.ActivityID)
|
||||
assert.Equal(t, 404, actionsError.StatusCode)
|
||||
|
||||
inner, ok := actionsError.Err.(*actionsExceptionError)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "exception-name", inner.ExceptionName)
|
||||
assert.Equal(t, "example error message", inner.Message)
|
||||
assert.Contains(t, err.Error(), "unknown error")
|
||||
assert.True(t, errors.Is(err, base))
|
||||
})
|
||||
|
||||
t.Run("wrapped exception error", func(t *testing.T) {
|
||||
errorMessage := `{"typeName":"exception-name","message":"example error message"}`
|
||||
response := &http.Response{
|
||||
ContentLength: int64(len(errorMessage)),
|
||||
StatusCode: 404,
|
||||
Header: http.Header{},
|
||||
Body: io.NopCloser(strings.NewReader(errorMessage)),
|
||||
t.Run("text/plain body is included", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
body := "example plain text error"
|
||||
resp := &http.Response{
|
||||
Status: "400 Bad Request",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
ContentLength: int64(len(body)),
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
}
|
||||
response.Header.Add(headerActionsActivityID, "activity-id")
|
||||
response.Header.Add("Content-Type", "application/json")
|
||||
resp.Header.Set("Content-Type", "text/plain")
|
||||
resp.Header.Set(headerActionsActivityID, "activity-id")
|
||||
|
||||
err := ParseActionsErrorFromResponse(response)
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), body)
|
||||
assert.True(t, errors.Is(err, base))
|
||||
})
|
||||
|
||||
var actionsExceptionError *actionsExceptionError
|
||||
assert.ErrorAs(t, err, &actionsExceptionError)
|
||||
t.Run("scalesetError in error chain uses raw body (no JSON parsing)", func(t *testing.T) {
|
||||
wrapped := fmt.Errorf("wrapped: %w", RunnerNotFoundError)
|
||||
body := `{"typeName":"AgentExistsException","message":"should not be parsed"}`
|
||||
resp := &http.Response{
|
||||
Status: "404 Not Found",
|
||||
StatusCode: http.StatusNotFound,
|
||||
ContentLength: int64(len(body)),
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
assert.Equal(t, "exception-name", actionsExceptionError.ExceptionName)
|
||||
assert.Equal(t, "example error message", actionsExceptionError.Message)
|
||||
err := newRequestResponseError(req(t), resp, wrapped)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, RunnerNotFoundError))
|
||||
assert.Contains(t, err.Error(), body)
|
||||
})
|
||||
|
||||
t.Run("known actions exception maps to sentinel error", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
jsonBody := `{"typeName":"AgentExistsException","message":"runner already exists"}`
|
||||
resp := &http.Response{
|
||||
Status: "409 Conflict",
|
||||
StatusCode: http.StatusConflict,
|
||||
ContentLength: int64(len(jsonBody)),
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(jsonBody)),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, RunnerExistsError))
|
||||
assert.False(t, errors.Is(err, base), "base error should not be wrapped for mapped exceptions")
|
||||
assert.Contains(t, err.Error(), "runner already exists")
|
||||
})
|
||||
|
||||
t.Run("agent not found exception maps to sentinel error", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
jsonBody := `{"typeName":"AgentNotFoundException","message":"missing"}`
|
||||
resp := &http.Response{
|
||||
Status: "404 Not Found",
|
||||
StatusCode: http.StatusNotFound,
|
||||
ContentLength: int64(len(jsonBody)),
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(jsonBody)),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, RunnerNotFoundError))
|
||||
assert.False(t, errors.Is(err, base))
|
||||
assert.Contains(t, err.Error(), "missing")
|
||||
})
|
||||
|
||||
t.Run("job still running exception maps to sentinel error", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
jsonBody := `{"typeName":"JobStillRunningException","message":"still running"}`
|
||||
resp := &http.Response{
|
||||
Status: "409 Conflict",
|
||||
StatusCode: http.StatusConflict,
|
||||
ContentLength: int64(len(jsonBody)),
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(jsonBody)),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, JobStillRunningError))
|
||||
assert.False(t, errors.Is(err, base))
|
||||
assert.Contains(t, err.Error(), "still running")
|
||||
})
|
||||
|
||||
t.Run("invalid json returns unmarshal error and includes body", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
bad := "not-json"
|
||||
resp := &http.Response{
|
||||
Status: "400 Bad Request",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
ContentLength: int64(len(bad)),
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(bad)),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "failed to unmarshal error response body")
|
||||
assert.Contains(t, err.Error(), "not-json")
|
||||
assert.False(t, errors.Is(err, base), "base error is not wrapped on JSON unmarshal failures")
|
||||
})
|
||||
|
||||
t.Run("unknown json error wraps exception", func(t *testing.T) {
|
||||
base := errors.New("base")
|
||||
jsonBody := `{"typeName":"SomeException","message":"example error message"}`
|
||||
resp := &http.Response{
|
||||
Status: "500 Internal Server Error",
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
ContentLength: int64(len(jsonBody)),
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(jsonBody)),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "application/json")
|
||||
|
||||
err := newRequestResponseError(req(t), resp, base)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, base))
|
||||
|
||||
var ex actionsExceptionError
|
||||
assert.True(t, errors.As(err, &ex))
|
||||
assert.Equal(t, "SomeException", ex.ExceptionName)
|
||||
assert.Equal(t, "example error message", ex.Message)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user