e4a017ce06
Co-authored-by: Francesco Renzi <rentziass@github.com> Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com>
254 lines
8.3 KiB
Go
254 lines
8.3 KiB
Go
package scaleset
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type readErrCloser struct{}
|
|
|
|
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{
|
|
ExceptionName: "exception-name",
|
|
Message: "example error message",
|
|
}
|
|
|
|
s := err.Error()
|
|
assert.Contains(t, s, "exception-name")
|
|
assert.Contains(t, s, "example error message")
|
|
})
|
|
}
|
|
|
|
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}
|
|
}
|
|
|
|
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("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,
|
|
}
|
|
|
|
err := newRequestResponseError(req(t), resp, base)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "unknown error")
|
|
assert.True(t, errors.Is(err, base))
|
|
})
|
|
|
|
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: make(http.Header),
|
|
}
|
|
resp.Header.Set(headerActionsActivityID, "activity-id")
|
|
resp.Header.Set(headerGitHubRequestID, "request-id")
|
|
|
|
err := newRequestResponseError(req(t), resp, base)
|
|
require.Error(t, err)
|
|
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("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{}),
|
|
}
|
|
|
|
err := newRequestResponseError(req(t), resp, base)
|
|
require.Error(t, err)
|
|
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("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("")),
|
|
}
|
|
|
|
err := newRequestResponseError(req(t), resp, base)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "unknown error")
|
|
assert.True(t, errors.Is(err, base))
|
|
})
|
|
|
|
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)),
|
|
}
|
|
resp.Header.Set("Content-Type", "text/plain")
|
|
resp.Header.Set(headerActionsActivityID, "activity-id")
|
|
|
|
err := newRequestResponseError(req(t), resp, base)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), body)
|
|
assert.True(t, errors.Is(err, base))
|
|
})
|
|
|
|
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")
|
|
|
|
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)
|
|
})
|
|
}
|