Files
scaleset/errors_test.go
Francesco Renzi e4a017ce06 Initial commit for open source release 🚀
Co-authored-by: Francesco Renzi <rentziass@github.com>
Co-authored-by: Nikola Jokic <jokicnikola07@gmail.com>
2026-02-03 16:41:15 +01:00

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