Add debug info while issuing request (#50)

* Add debug info while issuing request

* wip

* wip

* Rework tests
This commit is contained in:
Nikola Jokic
2026-01-30 13:03:22 +01:00
committed by GitHub
parent 145b7e382f
commit c841c96f1c
7 changed files with 423 additions and 612 deletions
+78 -165
View File
@@ -5,7 +5,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"maps"
@@ -296,30 +295,22 @@ func (c *Client) GetRunnerScaleSet(ctx context.Context, runnerGroupID int, runne
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, ParseActionsErrorFromResponse(resp)
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
var runnerScaleSetList *runnerScaleSetsResponse
var runnerScaleSetList runnerScaleSetsResponse
if err := json.NewDecoder(resp.Body).Decode(&runnerScaleSetList); err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode runner scale set list: %w", err))
}
if runnerScaleSetList.Count == 0 {
switch runnerScaleSetList.Count {
case 1:
return &runnerScaleSetList.RunnerScaleSets[0], nil
case 0:
return nil, nil
default:
return nil, newRequestResponseError(req, resp, fmt.Errorf("multiple runner scale sets found with name %q", runnerScaleSetName))
}
if runnerScaleSetList.Count > 1 {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: fmt.Errorf("multiple runner scale sets found with name %q", runnerScaleSetName),
}
}
return &runnerScaleSetList.RunnerScaleSets[0], nil
}
// GetRunnerScaleSetByID fetches a runner scale set by its ID.
@@ -340,16 +331,12 @@ func (c *Client) GetRunnerScaleSetByID(ctx context.Context, runnerScaleSetID int
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, ParseActionsErrorFromResponse(resp)
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
var runnerScaleSet *RunnerScaleSet
if err := json.NewDecoder(resp.Body).Decode(&runnerScaleSet); err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode runner scale set: %w", err))
}
return runnerScaleSet, nil
}
@@ -371,48 +358,22 @@ func (c *Client) GetRunnerGroupByName(ctx context.Context, runnerGroup string) (
}
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
}
return nil, fmt.Errorf("unexpected status code: %w", &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: errors.New(string(body)),
})
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
var runnerGroupList *RunnerGroupList
err = json.NewDecoder(resp.Body).Decode(&runnerGroupList)
if err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
var runnerGroupList RunnerGroupList
if err := json.NewDecoder(resp.Body).Decode(&runnerGroupList); err != nil {
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode runner group list: %w", err))
}
if runnerGroupList.Count == 0 {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: fmt.Errorf("no runner group found with name %q", runnerGroup),
}
switch runnerGroupList.Count {
case 1:
return &runnerGroupList.RunnerGroups[0], nil
case 0:
return nil, newRequestResponseError(req, resp, fmt.Errorf("no runner group found with name %q", runnerGroup))
default:
return nil, newRequestResponseError(req, resp, fmt.Errorf("multiple runner group found with name %q", runnerGroup))
}
if runnerGroupList.Count > 1 {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: fmt.Errorf("multiple runner group found with name %q", runnerGroup),
}
}
return &runnerGroupList.RunnerGroups[0], nil
}
// applyDefaultLabelTypes ensures that each label in the runner scale set has a Type set,
@@ -449,17 +410,15 @@ func (c *Client) CreateRunnerScaleSet(ctx context.Context, runnerScaleSet *Runne
}
if resp.StatusCode != http.StatusOK {
return nil, ParseActionsErrorFromResponse(resp)
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
var createdRunnerScaleSet *RunnerScaleSet
var createdRunnerScaleSet RunnerScaleSet
if err := json.NewDecoder(resp.Body).Decode(&createdRunnerScaleSet); err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode created runner scale set: %w", err))
}
return createdRunnerScaleSet, nil
return &createdRunnerScaleSet, nil
}
// UpdateRunnerScaleSet updates an existing runner scale set.
@@ -487,18 +446,14 @@ func (c *Client) UpdateRunnerScaleSet(ctx context.Context, runnerScaleSetID int,
}
if resp.StatusCode != http.StatusOK {
return nil, ParseActionsErrorFromResponse(resp)
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
var updatedRunnerScaleSet *RunnerScaleSet
var updatedRunnerScaleSet RunnerScaleSet
if err := json.NewDecoder(resp.Body).Decode(&updatedRunnerScaleSet); err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode updated runner scale set: %w", err))
}
return updatedRunnerScaleSet, nil
return &updatedRunnerScaleSet, nil
}
// DeleteRunnerScaleSet deletes a runner scale set by its ID.
@@ -519,7 +474,7 @@ func (c *Client) DeleteRunnerScaleSet(ctx context.Context, runnerScaleSetID int)
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return ParseActionsErrorFromResponse(resp)
return newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
return nil
@@ -644,17 +599,14 @@ func (c *Client) GenerateJitRunnerConfig(ctx context.Context, jitRunnerSetting *
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, ParseActionsErrorFromResponse(resp)
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
var runnerJitConfig *RunnerScaleSetJitRunnerConfig
if err := json.NewDecoder(resp.Body).Decode(&runnerJitConfig); err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode runner JIT config: %w", err))
}
return runnerJitConfig, nil
}
@@ -677,16 +629,12 @@ func (c *Client) GetRunner(ctx context.Context, runnerID int) (*RunnerReference,
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, ParseActionsErrorFromResponse(resp)
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
var runnerReference *RunnerReference
if err := json.NewDecoder(resp.Body).Decode(&runnerReference); err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode runner reference: %w", err))
}
return runnerReference, nil
@@ -711,31 +659,22 @@ func (c *Client) GetRunnerByName(ctx context.Context, runnerName string) (*Runne
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, ParseActionsErrorFromResponse(resp)
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
var runnerList *RunnerReferenceList
if err := json.NewDecoder(resp.Body).Decode(&runnerList); err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode runner reference list: %w", err))
}
if runnerList.Count == 0 {
switch runnerList.Count {
case 1:
return &runnerList.RunnerReferences[0], nil
case 0:
return nil, nil
default:
return nil, fmt.Errorf("multiple runners found with name %q", runnerName)
}
if runnerList.Count > 1 {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: fmt.Errorf("multiple runner found with name %s", runnerName),
}
}
return &runnerList.RunnerReferences[0], nil
}
// RemoveRunner removes a runner by its ID.
@@ -757,7 +696,7 @@ func (c *Client) RemoveRunner(ctx context.Context, runnerID int64) error {
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return ParseActionsErrorFromResponse(resp)
return newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
return nil
@@ -805,24 +744,12 @@ func (c *Client) getRunnerRegistrationToken(ctx context.Context) (*registrationT
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read the body: %w", err)
}
return nil, &GitHubAPIError{
StatusCode: resp.StatusCode,
RequestID: resp.Header.Get(headerGitHubRequestID),
Err: errors.New(string(body)),
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to get runner registration token (%v)", resp.Status))
}
var registrationToken *registrationToken
if err := json.NewDecoder(resp.Body).Decode(&registrationToken); err != nil {
return nil, &GitHubAPIError{
StatusCode: resp.StatusCode,
RequestID: resp.Header.Get(headerGitHubRequestID),
Err: err,
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode runner registration token: %w", err))
}
return registrationToken, nil
@@ -858,28 +785,15 @@ func (c *Client) fetchAccessToken(ctx context.Context, creds *GitHubAppAuth) (*a
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
errMsg := fmt.Sprintf("failed to get access token for GitHub App auth (%v)", resp.Status)
if body, err := io.ReadAll(resp.Body); err == nil {
errMsg = fmt.Sprintf("%s: %s", errMsg, string(body))
}
return nil, &GitHubAPIError{
StatusCode: resp.StatusCode,
RequestID: resp.Header.Get(headerGitHubRequestID),
Err: errors.New(errMsg),
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to get access token for GitHub App auth (%v)", resp.Status))
}
// Format: https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app
var accessToken *accessToken
if err = json.NewDecoder(resp.Body).Decode(&accessToken); err != nil {
return nil, &GitHubAPIError{
StatusCode: resp.StatusCode,
RequestID: resp.Header.Get(headerGitHubRequestID),
Err: err,
}
var accessToken accessToken
if err := json.NewDecoder(resp.Body).Decode(&accessToken); err != nil {
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode access token for GitHub App auth: %w", err))
}
return accessToken, nil
return &accessToken, nil
}
type actionsServiceAdminConnection struct {
@@ -944,38 +858,28 @@ func (c *Client) getActionsServiceAdminConnectionRequest(req *http.Request) (*ac
}
httpClient := retryableClient.StandardClient()
resp, err := httpClient.Do(req)
resp, err := sendRequest(httpClient, req)
if err != nil {
return nil, fmt.Errorf("failed to issue the request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
var actionsServiceAdminConnection *actionsServiceAdminConnection
if err := json.NewDecoder(resp.Body).Decode(&actionsServiceAdminConnection); err != nil {
return nil, &GitHubAPIError{
StatusCode: resp.StatusCode,
RequestID: resp.Header.Get(headerGitHubRequestID),
Err: err,
}
}
return actionsServiceAdminConnection, nil
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code: %d", resp.StatusCode))
}
var innerErr error
body, err := io.ReadAll(resp.Body)
if err != nil {
innerErr = err
} else {
innerErr = errors.New(string(body))
var actionsServiceAdminConnection actionsServiceAdminConnection
if err := json.NewDecoder(resp.Body).Decode(&actionsServiceAdminConnection); err != nil {
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to decode actions service admin connection: %w", err))
}
if actionsServiceAdminConnection.ActionsServiceURL == nil || *actionsServiceAdminConnection.ActionsServiceURL == "" {
return nil, fmt.Errorf("actions service admin connection missing url")
}
if actionsServiceAdminConnection.AdminToken == nil || *actionsServiceAdminConnection.AdminToken == "" {
return nil, fmt.Errorf("actions service admin connection missing token")
}
return nil, &GitHubAPIError{
StatusCode: resp.StatusCode,
RequestID: resp.Header.Get(headerGitHubRequestID),
Err: innerErr,
}
return &actionsServiceAdminConnection, nil
}
func createRegistrationTokenPath(config *gitHubConfig) (string, error) {
@@ -1049,7 +953,13 @@ func (c *Client) updateTokenIfNeeded(ctx context.Context) error {
return nil
}
c.logger.Info("refreshing token", "githubConfigUrl", c.config.configURL.String())
configURL := ""
if c.config.configURL != nil {
configURL = c.config.configURL.String()
}
if c.logger != nil {
c.logger.Info("refreshing token", "githubConfigUrl", configURL)
}
rt, err := c.getRunnerRegistrationToken(ctx)
if err != nil {
return fmt.Errorf("failed to get runner registration token on refresh: %w", err)
@@ -1059,6 +969,9 @@ func (c *Client) updateTokenIfNeeded(ctx context.Context) error {
if err != nil {
return fmt.Errorf("failed to get actions service admin connection on refresh: %w", err)
}
if adminConnInfo == nil || adminConnInfo.ActionsServiceURL == nil || adminConnInfo.AdminToken == nil {
return fmt.Errorf("failed to get actions service admin connection on refresh: missing url or token")
}
c.actionsServiceURL = *adminConnInfo.ActionsServiceURL
c.actionsServiceAdminToken = *adminConnInfo.AdminToken
+14 -12
View File
@@ -176,7 +176,7 @@ func TestNewActionsServiceRequest(t *testing.T) {
client.actionsServiceAdminTokenExpiresAt = expiresAt
_, err = client.newActionsServiceRequest(ctx, http.MethodGet, "my-path", nil)
require.Error(t, err)
assert.Contains(t, err.Error(), errMessage)
assert.Contains(t, err.Error(), "test")
assert.Equal(t, client.actionsServiceAdminToken, expiringToken)
assert.Equal(t, client.actionsServiceAdminTokenExpiresAt, expiresAt)
})
@@ -604,9 +604,11 @@ func TestGetRunnerScaleSet(t *testing.T) {
})
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
plainBody := "example plain text error"
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(plainBody))
}))
client, err := newClient(
@@ -666,11 +668,6 @@ func TestGetRunnerScaleSet(t *testing.T) {
t.Run("Multiple runner scale sets found", func(t *testing.T) {
reqID := uuid.NewString()
wantErr := &ActionsError{
StatusCode: http.StatusOK,
ActivityID: reqID,
Err: fmt.Errorf("multiple runner scale sets found with name %q", scaleSetName),
}
runnerScaleSetsResp := []byte(`{"count":2,"value":[{"id":1,"name":"ScaleSet"}]}`)
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set(headerActionsActivityID, reqID)
@@ -686,7 +683,8 @@ func TestGetRunnerScaleSet(t *testing.T) {
_, err = client.GetRunnerScaleSet(ctx, 1, scaleSetName)
require.NotNil(t, err)
assert.Equal(t, wantErr.Error(), err.Error())
assert.Contains(t, err.Error(), "multiple runner scale sets found")
assert.Contains(t, err.Error(), "activity_id=\""+reqID+"\"")
})
}
@@ -761,9 +759,11 @@ func TestGetRunnerScaleSetByID(t *testing.T) {
})
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
plainBody := "example plain text error"
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(plainBody))
}))
client, err := newClient(
@@ -876,9 +876,11 @@ func TestCreateRunnerScaleSet(t *testing.T) {
})
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
plainBody := "example plain text error"
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(plainBody))
}))
client, err := newClient(
@@ -890,8 +892,8 @@ func TestCreateRunnerScaleSet(t *testing.T) {
_, err = client.CreateRunnerScaleSet(ctx, &runnerScaleSet)
require.NotNil(t, err)
var expectedErr *ActionsError
assert.True(t, errors.As(err, &expectedErr))
assert.Contains(t, err.Error(), "status=\"400 Bad Request\"")
assert.Contains(t, err.Error(), plainBody)
})
t.Run("Default retries on server error", func(t *testing.T) {
+18 -6
View File
@@ -14,6 +14,11 @@ import (
"github.com/hashicorp/go-retryablehttp"
)
const (
headerActionsActivityID = "ActivityId"
headerGitHubRequestID = "X-GitHub-Request-Id"
)
type commonClient struct {
httpClient *http.Client
@@ -44,17 +49,24 @@ func (c *commonClient) newRetryableHTTPClient() (*retryablehttp.Client, error) {
}
func (c *commonClient) do(req *http.Request) (*http.Response, error) {
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("client request failed: %w", err)
}
return sendRequest(c.httpClient, req)
}
// sendRequest ensures that the request is sent and the response body is fully read and closed.
// It trims the BOM when present in the response body.
//
// Make sure to use this function instead of http.Client.Do directly to avoid issues.
func sendRequest(c *http.Client, req *http.Request) (*http.Response, error) {
resp, err := c.Do(req)
if err != nil {
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to send request: %w", err))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read the response body: %w", err)
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to read the response body: %w", err))
}
if err := resp.Body.Close(); err != nil {
return nil, fmt.Errorf("failed to close the response body: %w", err)
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to close the response body: %w", err))
}
body = trimByteOrderMark(body)
+64 -111
View File
@@ -9,137 +9,90 @@ import (
"strings"
)
// Header names for request IDs
const (
headerActionsActivityID = "ActivityId"
headerGitHubRequestID = "X-GitHub-Request-Id"
type scalesetError string
func (e scalesetError) Error() string {
return string(e)
}
var (
RunnerNotFoundError = scalesetError("runner not found")
RunnerExistsError = scalesetError("runner exists")
JobStillRunningError = scalesetError("job still running")
MessageQueueTokenExpiredError = scalesetError("message queue token expired")
)
type GitHubAPIError struct {
StatusCode int
RequestID string
Err error
}
func (e *GitHubAPIError) Error() string {
return fmt.Sprintf("github api error: StatusCode %d, RequestID %q: %v", e.StatusCode, e.RequestID, e.Err)
}
func (e *GitHubAPIError) Unwrap() error {
return e.Err
}
type ActionsError struct {
ActivityID string
StatusCode int
Err error
}
func (e *ActionsError) Error() string {
return fmt.Sprintf("actions error: StatusCode %d, ActivityId %q: %v", e.StatusCode, e.ActivityID, e.Err)
}
func (e *ActionsError) Unwrap() error {
return e.Err
}
func (e *ActionsError) IsAgentNotFound() bool {
return e.isException("AgentNotFoundException")
}
func (e *ActionsError) IsJobStillRunning() bool {
return e.isException("JobStillRunningException")
}
func (e *ActionsError) IsMessageQueueTokenExpired() bool {
if e == nil {
return false
}
var err *messageQueueTokenExpiredError
return errors.As(e.Err, &err)
}
func (e *ActionsError) IsAgentExists() bool {
return e.isException("AgentExistsException")
}
func (e *ActionsError) isException(target string) bool {
if e == nil {
return false
}
if ex, ok := e.Err.(*actionsExceptionError); ok {
return strings.Contains(ex.ExceptionName, target)
}
return false
}
type actionsExceptionError struct {
ExceptionName string `json:"typeName,omitempty"`
Message string `json:"message,omitempty"`
}
func (e *actionsExceptionError) Error() string {
func (e actionsExceptionError) Error() string {
return fmt.Sprintf("%s: %s", e.ExceptionName, e.Message)
}
func ParseActionsErrorFromResponse(response *http.Response) error {
if response.ContentLength == 0 {
return &ActionsError{
ActivityID: response.Header.Get(headerActionsActivityID),
StatusCode: response.StatusCode,
Err: errors.New("unknown exception"),
}
// newRequestResponseError creates a detailed error message based on the HTTP request and response,
// including parsing the response body for known error formats.
//
// The sendRequest already parses errors using this method, so use this error if the client doesn't
// return an error, but the error is happening on the application logic level.
//
// Prefer creating errors using this function instead of manually constructing error messages since it automatically
// includes useful metadata like activity IDs and request IDs, and handles well-known error cases.
func newRequestResponseError(req *http.Request, resp *http.Response, err error) error {
var sb strings.Builder
fmt.Fprintf(&sb, "request %s %s failed", req.Method, req.URL.String())
if resp == nil {
return fmt.Errorf("%s: %w", sb.String(), err)
}
body, err := io.ReadAll(response.Body)
if err != nil {
return &ActionsError{
ActivityID: response.Header.Get(headerActionsActivityID),
StatusCode: response.StatusCode,
Err: err,
}
sb.WriteRune('(')
fmt.Fprintf(&sb, "status=%q", resp.Status)
if resp.Header.Get(headerActionsActivityID) != "" {
fmt.Fprintf(&sb, ", activity_id=%q", resp.Header.Get(headerActionsActivityID))
}
body = trimByteOrderMark(body)
contentType := response.Header.Get("Content-Type")
if resp.Header.Get(headerGitHubRequestID) != "" {
fmt.Fprintf(&sb, ", github_request_id=%q", resp.Header.Get(headerGitHubRequestID))
}
sb.WriteRune(')')
if resp.Body == nil || resp.ContentLength == 0 {
return fmt.Errorf("%s: %w: unknown error", sb.String(), err)
}
body, bodyErr := io.ReadAll(resp.Body)
if bodyErr != nil {
return fmt.Errorf("%s: %w: failed to read error response body: %w", sb.String(), err, bodyErr)
}
if len(body) == 0 {
return fmt.Errorf("%s: %w: unknown error", sb.String(), err)
}
var scalesetErr scalesetError
if errors.As(err, &scalesetErr) {
return fmt.Errorf("%s: %w: %s", sb.String(), err, string(body))
}
contentType := resp.Header.Get("Content-Type")
if len(contentType) > 0 && strings.Contains(contentType, "text/plain") {
message := string(body)
return &ActionsError{
ActivityID: response.Header.Get(headerActionsActivityID),
StatusCode: response.StatusCode,
Err: errors.New(message),
}
return fmt.Errorf("%s: %w: %s", sb.String(), err, string(body))
}
var exception actionsExceptionError
if err := json.Unmarshal(body, &exception); err != nil {
return &ActionsError{
ActivityID: response.Header.Get(headerActionsActivityID),
StatusCode: response.StatusCode,
Err: err,
}
return fmt.Errorf("%s: %w: failed to unmarshal error response body: %q", sb.String(), err, string(body))
}
return &ActionsError{
ActivityID: response.Header.Get(headerActionsActivityID),
StatusCode: response.StatusCode,
Err: &exception,
}
}
type messageQueueTokenExpiredError struct {
message string
}
func (e *messageQueueTokenExpiredError) Error() string {
return fmt.Sprintf("message queue token expired: %s", e.message)
}
// NewMessageQueueTokenExpiredError creates a new MessageQueueTokenExpiredError.
//
// This function is mostly used by tests.
func NewMessageQueueTokenExpiredError(message string) error {
return &messageQueueTokenExpiredError{
message: message,
switch {
case strings.Contains(exception.ExceptionName, "AgentExistsException"):
return fmt.Errorf("%s: %w: %s", sb.String(), RunnerExistsError, exception.Message)
case strings.Contains(exception.ExceptionName, "AgentNotFoundException"):
return fmt.Errorf("%s: %w: %s", sb.String(), RunnerNotFoundError, exception.Message)
case strings.Contains(exception.ExceptionName, "JobStillRunningException"):
return fmt.Errorf("%s: %w: %s", sb.String(), JobStillRunningError, exception.Message)
default:
return fmt.Errorf("%s: %w: %w", sb.String(), err, exception)
}
}
+196 -195
View File
@@ -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)
})
}
+22 -81
View File
@@ -104,8 +104,7 @@ func (c *MessageSessionClient) GetMessage(ctx context.Context, lastMessageID int
return message, nil
}
expiredError := &ActionsError{}
if !errors.As(err, &expiredError) || !expiredError.IsMessageQueueTokenExpired() {
if !errors.Is(err, MessageQueueTokenExpiredError) {
return nil, fmt.Errorf("failed to get next message: %w", err)
}
@@ -148,43 +147,23 @@ func (c *MessageSessionClient) getMessage(ctx context.Context, lastMessageID int
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusAccepted {
switch resp.StatusCode {
case http.StatusAccepted:
return nil, nil
}
if resp.StatusCode != http.StatusOK {
if resp.StatusCode != http.StatusUnauthorized {
return nil, ParseActionsErrorFromResponse(resp)
}
body, err := io.ReadAll(resp.Body)
body = trimByteOrderMark(body)
case http.StatusOK:
message, err := parseRunnerScaleSetMessageResponse(resp.Body)
if err != nil {
return nil, &ActionsError{
ActivityID: resp.Header.Get(headerActionsActivityID),
StatusCode: resp.StatusCode,
Err: err,
}
return nil, newRequestResponseError(req, resp, fmt.Errorf("failed to parse message response: %w", err))
}
return nil, &ActionsError{
ActivityID: resp.Header.Get(headerActionsActivityID),
StatusCode: resp.StatusCode,
Err: &messageQueueTokenExpiredError{
message: string(body),
},
}
}
return message, nil
message, err := parseRunnerScaleSetMessageResponse(resp.Body)
if err != nil {
return nil, &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
}
case http.StatusUnauthorized:
return nil, newRequestResponseError(req, resp, MessageQueueTokenExpiredError)
return message, nil
default:
return nil, newRequestResponseError(req, resp, fmt.Errorf("unexpected status code %s", resp.Status))
}
}
// DeleteMessage deletes a message from the runner scale set message queue.
@@ -199,8 +178,7 @@ func (c *MessageSessionClient) DeleteMessage(ctx context.Context, messageID int)
return nil
}
expiredError := &ActionsError{}
if !errors.As(err, &expiredError) || !expiredError.IsMessageQueueTokenExpired() {
if !errors.Is(err, MessageQueueTokenExpiredError) {
return fmt.Errorf("failed to delete message: %w", err)
}
@@ -239,25 +217,10 @@ func (c *MessageSessionClient) deleteMessage(ctx context.Context, messageID int)
}
if resp.StatusCode != http.StatusUnauthorized {
return ParseActionsErrorFromResponse(resp)
return newRequestResponseError(req, resp, fmt.Errorf("unexpected status code %s", resp.Status))
}
body, err := io.ReadAll(resp.Body)
body = trimByteOrderMark(body)
if err != nil {
return &ActionsError{
ActivityID: resp.Header.Get(headerActionsActivityID),
StatusCode: resp.StatusCode,
Err: err,
}
}
return &ActionsError{
ActivityID: resp.Header.Get(headerActionsActivityID),
StatusCode: resp.StatusCode,
Err: &messageQueueTokenExpiredError{
message: string(body),
},
}
return newRequestResponseError(req, resp, MessageQueueTokenExpiredError)
}
func (c *MessageSessionClient) Session() RunnerScaleSetSession {
@@ -284,39 +247,17 @@ func (c *MessageSessionClient) doSessionRequest(ctx context.Context, method, pat
}
defer resp.Body.Close()
if resp.StatusCode == expectedResponseStatusCode {
if responseUnmarshalTarget == nil {
return nil
}
if err := json.NewDecoder(resp.Body).Decode(responseUnmarshalTarget); err != nil {
return &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
}
if resp.StatusCode != expectedResponseStatusCode {
return newRequestResponseError(req, resp, fmt.Errorf("unexpected status code %s", resp.Status))
}
if responseUnmarshalTarget == nil {
return nil
}
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
return ParseActionsErrorFromResponse(resp)
if err := json.NewDecoder(resp.Body).Decode(responseUnmarshalTarget); err != nil {
return newRequestResponseError(req, resp, fmt.Errorf("failed to unmarshal response body: %w", err))
}
body, err := io.ReadAll(resp.Body)
body = trimByteOrderMark(body)
if err != nil {
return &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: err,
}
}
return fmt.Errorf("unexpected status code: %w", &ActionsError{
StatusCode: resp.StatusCode,
ActivityID: resp.Header.Get(headerActionsActivityID),
Err: errors.New(string(body)),
})
return nil
}
+31 -42
View File
@@ -62,7 +62,7 @@ func TestCreateMessageSession(t *testing.T) {
assert.Equal(t, want, session)
})
t.Run("CreateMessageSession unmarshals errors into ActionsError", func(t *testing.T) {
t.Run("CreateMessageSession includes actions exception details", func(t *testing.T) {
owner := "foo"
runnerScaleSet := RunnerScaleSet{
ID: 1,
@@ -71,15 +71,6 @@ func TestCreateMessageSession(t *testing.T) {
RunnerSetting: RunnerSetting{},
}
want := &ActionsError{
ActivityID: exampleRequestID,
StatusCode: http.StatusBadRequest,
Err: &actionsExceptionError{
ExceptionName: "CSharpExceptionNameHere",
Message: "could not do something",
},
}
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set(headerActionsActivityID, exampleRequestID)
@@ -97,11 +88,14 @@ func TestCreateMessageSession(t *testing.T) {
sessionClient, err := client.MessageSessionClient(context.Background(), runnerScaleSet.ID, owner)
assert.Nil(t, sessionClient)
require.Error(t, err)
assert.Contains(t, err.Error(), "status=\"400 Bad Request\"")
assert.Contains(t, err.Error(), "activity_id=\""+exampleRequestID+"\"")
errorTypeForComparison := &ActionsError{}
assert.ErrorAs(t, err, &errorTypeForComparison)
assert.Equal(t, want, errorTypeForComparison)
var ex actionsExceptionError
assert.True(t, errors.As(err, &ex))
assert.Equal(t, "CSharpExceptionNameHere", ex.ExceptionName)
assert.Equal(t, "could not do something", ex.Message)
})
t.Run("CreateMessageSession call is retried the correct amount of times", func(t *testing.T) {
@@ -285,10 +279,7 @@ func TestGetMessage(t *testing.T) {
msg, err := sessionClient.GetMessage(ctx, 0, 10)
assert.Nil(t, msg)
var expectedErr *ActionsError
require.ErrorAs(t, err, &expectedErr)
assert.True(t, expectedErr.IsMessageQueueTokenExpired(), "expected error to be of type MessageQueueTokenExpiredError but got: %v", err)
assert.ErrorIs(t, err, MessageQueueTokenExpiredError, "expected error to be MessageQueueTokenExpiredError but got: %v", err)
})
t.Run("Message token refreshed", func(t *testing.T) {
@@ -345,10 +336,6 @@ func TestGetMessage(t *testing.T) {
})
t.Run("Status code not found", func(t *testing.T) {
want := ActionsError{
Err: errors.New("unknown exception"),
StatusCode: 404,
}
var handleSessionRequest http.HandlerFunc
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "sessions") {
@@ -371,21 +358,22 @@ func TestGetMessage(t *testing.T) {
msg, err := sessionClient.GetMessage(ctx, 0, 10)
assert.Nil(t, msg)
var got *ActionsError
require.ErrorAs(t, err, &got)
assert.Equal(t, want.StatusCode, got.StatusCode)
assert.Equal(t, want.Err.Error(), got.Err.Error())
require.Error(t, err)
assert.Contains(t, err.Error(), "status=\"404 Not Found\"")
assert.Contains(t, err.Error(), "unknown error")
})
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
plainBody := "example plain text error"
var handleSessionRequest http.HandlerFunc
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "sessions") {
handleSessionRequest(w, r)
return
}
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(plainBody))
}))
handleSessionRequest = newTestSessionRequestHandler(t, server.testRunnerScaleSetSession())
@@ -402,9 +390,12 @@ func TestGetMessage(t *testing.T) {
msg, err := sessionClient.GetMessage(ctx, 0, 10)
assert.Nil(t, msg)
assert.NotNil(t, err)
assert.Contains(t, err.Error(), "status=\"400 Bad Request\"")
assert.Contains(t, err.Error(), plainBody)
})
t.Run("Capacity error handling", func(t *testing.T) {
plainBody := "capacity error"
var handleSessionRequest http.HandlerFunc
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "sessions") {
@@ -415,9 +406,9 @@ func TestGetMessage(t *testing.T) {
c, err := strconv.Atoi(hc)
require.NoError(t, err)
assert.GreaterOrEqual(t, c, 0)
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(plainBody))
}))
handleSessionRequest = newTestSessionRequestHandler(t, server.testRunnerScaleSetSession())
@@ -434,9 +425,8 @@ func TestGetMessage(t *testing.T) {
msg, err := sessionClient.GetMessage(ctx, 0, 0)
assert.Nil(t, msg)
assert.Error(t, err)
var expectedErr *ActionsError
assert.ErrorAs(t, err, &expectedErr)
assert.Equal(t, http.StatusBadRequest, expectedErr.StatusCode)
assert.Contains(t, err.Error(), "status=\"400 Bad Request\"")
assert.Contains(t, err.Error(), plainBody)
})
}
@@ -506,9 +496,7 @@ func TestDeleteMessage(t *testing.T) {
err = sessionClient.DeleteMessage(ctx, 0)
require.NotNil(t, err)
var expectedErr *ActionsError
require.ErrorAs(t, err, &expectedErr)
assert.True(t, expectedErr.IsMessageQueueTokenExpired())
assert.ErrorIs(t, err, MessageQueueTokenExpiredError, "expected error to be MessageQueueTokenExpiredError but got: %v", err)
})
t.Run("message token refreshed", func(t *testing.T) {
@@ -563,14 +551,16 @@ func TestDeleteMessage(t *testing.T) {
})
t.Run("Error when Content-Type is text/plain", func(t *testing.T) {
plainBody := "example plain text error"
var handleSessionRequest http.HandlerFunc
server := newActionsServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "sessions") {
handleSessionRequest(w, r)
return
}
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(plainBody))
}))
handleSessionRequest = newTestSessionRequestHandler(t, server.testRunnerScaleSetSession())
@@ -586,10 +576,9 @@ func TestDeleteMessage(t *testing.T) {
err = sessionClient.DeleteMessage(ctx, runnerScaleSetMessage.MessageID)
require.NotNil(t, err)
var expectedErr *ActionsError
assert.True(t, errors.As(err, &expectedErr))
},
)
assert.Contains(t, err.Error(), "status=\"400 Bad Request\"")
assert.Contains(t, err.Error(), plainBody)
})
t.Run("Default retries on server error", func(t *testing.T) {
actualRetry := 0
@@ -656,7 +645,7 @@ func TestDeleteMessage(t *testing.T) {
require.NoError(t, err)
err = sessionClient.DeleteMessage(ctx, runnerScaleSetMessage.MessageID+1)
var expectedErr *ActionsError
require.True(t, errors.As(err, &expectedErr))
require.Error(t, err)
assert.Contains(t, err.Error(), "unexpected status code")
})
}