Skip to content

Commit e22526c

Browse files
committed
Introduce ErrOrganizationNotFound and refactor status handling and common errors
1 parent 67089d7 commit e22526c

16 files changed

Lines changed: 660 additions & 390 deletions

File tree

apierror/apierror.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Package apierror defines cross-cutting error values that the CREC SDK returns
2+
// when the API responds with a recognizable application-level error.
3+
package apierror
4+
5+
import (
6+
"errors"
7+
"fmt"
8+
9+
apiClient "github.com/smartcontractkit/crec-api-go/client"
10+
)
11+
12+
// ErrOrganizationNotFound is returned when the CREC API reports that the
13+
// authenticated organization is not onboarded in CRE Connect (HTTP 401 with an
14+
// ApplicationError of type ORGANIZATION_NOT_FOUND).
15+
var ErrOrganizationNotFound = errors.New("organization not found")
16+
17+
// ErrUnexpectedStatusCode is returned when the API responds with an HTTP status
18+
// the SDK does not handle explicitly.
19+
var ErrUnexpectedStatusCode = errors.New("unexpected status code")
20+
21+
// ErrNilResponse is returned when the API response is nil.
22+
var ErrNilResponse = errors.New("unexpected nil response")
23+
24+
// ErrNilResponseBody is returned when the API response body is nil.
25+
var ErrNilResponseBody = errors.New("unexpected nil response body")
26+
27+
// FromApplicationError maps a known ApplicationError to its canonical SDK
28+
// sentinel error, or returns nil when the error has no dedicated mapping.
29+
func FromApplicationError(appErr *apiClient.ApplicationError) error {
30+
if appErr == nil {
31+
return nil
32+
}
33+
34+
switch appErr.Type {
35+
case apiClient.ORGANIZATIONNOTFOUND:
36+
return ErrOrganizationNotFound
37+
default:
38+
return nil
39+
}
40+
}
41+
42+
// Wrap returns an error wrapping opErr with the canonical sentinel for appErr's
43+
// type. If appErr has no dedicated mapping, it falls back to a generic
44+
// unexpected-status error wrapping [ErrUnexpectedStatusCode] with statusCode
45+
// for diagnostics.
46+
func Wrap(appErr *apiClient.ApplicationError, opErr error, statusCode int) error {
47+
if mapped := FromApplicationError(appErr); mapped != nil {
48+
return fmt.Errorf("%w: %w", opErr, mapped)
49+
}
50+
return fmt.Errorf("%w: %w (status code %d)", opErr, ErrUnexpectedStatusCode, statusCode)
51+
}

apierror/apierror_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package apierror_test
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"testing"
7+
8+
apiClient "github.com/smartcontractkit/crec-api-go/client"
9+
"github.com/stretchr/testify/assert"
10+
11+
"github.com/smartcontractkit/crec-sdk/apierror"
12+
)
13+
14+
func TestApierror_FromApplicationError(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
appErr *apiClient.ApplicationError
18+
wantErr error
19+
}{
20+
{
21+
name: "nil application error returns nil",
22+
appErr: nil,
23+
wantErr: nil,
24+
},
25+
{
26+
name: "organization not found maps to sentinel",
27+
appErr: &apiClient.ApplicationError{Type: apiClient.ORGANIZATIONNOTFOUND, Message: "organization not found"},
28+
wantErr: apierror.ErrOrganizationNotFound,
29+
},
30+
{
31+
name: "unknown future type degrades to nil",
32+
appErr: &apiClient.ApplicationError{Type: "SOME_FUTURE_TYPE", Message: "new"},
33+
wantErr: nil,
34+
},
35+
}
36+
37+
for _, tt := range tests {
38+
t.Run(tt.name, func(t *testing.T) {
39+
got := apierror.FromApplicationError(tt.appErr)
40+
41+
if tt.wantErr == nil {
42+
assert.NoError(t, got)
43+
return
44+
}
45+
assert.ErrorIs(t, got, tt.wantErr)
46+
})
47+
}
48+
}
49+
50+
func TestApierror_Wrap(t *testing.T) {
51+
opErr := errors.New("operation failed")
52+
53+
t.Run("mapped sentinel wraps opErr", func(t *testing.T) {
54+
appErr := &apiClient.ApplicationError{Type: apiClient.ORGANIZATIONNOTFOUND, Message: "organization not found"}
55+
err := apierror.Wrap(appErr, opErr, http.StatusUnauthorized)
56+
57+
assert.ErrorIs(t, err, opErr)
58+
assert.ErrorIs(t, err, apierror.ErrOrganizationNotFound)
59+
assert.NotErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
60+
})
61+
62+
t.Run("unmapped type falls back to unexpected-status error", func(t *testing.T) {
63+
appErr := &apiClient.ApplicationError{Type: "SOME_FUTURE_TYPE", Message: "new"}
64+
err := apierror.Wrap(appErr, opErr, http.StatusUnauthorized)
65+
66+
assert.ErrorIs(t, err, opErr)
67+
assert.ErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
68+
assert.NotErrorIs(t, err, apierror.ErrOrganizationNotFound)
69+
})
70+
71+
t.Run("nil application error falls back to unexpected-status error", func(t *testing.T) {
72+
err := apierror.Wrap(nil, opErr, http.StatusUnauthorized)
73+
74+
assert.ErrorIs(t, err, opErr)
75+
assert.ErrorIs(t, err, apierror.ErrUnexpectedStatusCode)
76+
})
77+
}

channels/channels.go

Lines changed: 97 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"errors"
66
"fmt"
77
"log/slog"
8+
"net/http"
89
"strings"
910

1011
"github.com/google/uuid"
1112

1213
apiClient "github.com/smartcontractkit/crec-api-go/client"
14+
15+
"github.com/smartcontractkit/crec-sdk/apierror"
1316
)
1417

1518
const (
@@ -35,13 +38,6 @@ var (
3538
ErrListChannels = errors.New("failed to list channels")
3639
ErrUpdateChannel = errors.New("failed to update channel")
3740
ErrArchiveChannel = errors.New("failed to archive channel")
38-
39-
// Response errors
40-
ErrUnexpectedStatusCode = errors.New("unexpected status code")
41-
// ErrNilResponse is returned when the API response is nil.
42-
ErrNilResponse = errors.New("unexpected nil response")
43-
// ErrNilResponseBody is returned when the API response body is nil.
44-
ErrNilResponseBody = errors.New("unexpected nil response body")
4541
)
4642

4743
// Options defines the options for creating a new CREC Channels client.
@@ -129,25 +125,29 @@ func (c *Client) Create(ctx context.Context, input CreateInput) (*apiClient.Chan
129125
}
130126

131127
if resp == nil {
132-
return nil, fmt.Errorf("%w: %w", ErrCreateChannel, ErrNilResponse)
133-
}
134-
135-
if resp.StatusCode() != 201 {
128+
return nil, fmt.Errorf("%w: %w", ErrCreateChannel, apierror.ErrNilResponse)
129+
}
130+
131+
switch resp.StatusCode() {
132+
case http.StatusCreated:
133+
if resp.JSON201 == nil {
134+
return nil, fmt.Errorf("%w: %w", ErrCreateChannel, apierror.ErrNilResponseBody)
135+
}
136+
c.logger.Info("Channel created successfully",
137+
"channel_id", resp.JSON201.ChannelId.String(),
138+
"name", resp.JSON201.Name)
139+
return resp.JSON201, nil
140+
case http.StatusUnauthorized:
141+
c.logger.Error("Unauthorized when creating channel",
142+
"status_code", resp.StatusCode(),
143+
"body", string(resp.Body))
144+
return nil, apierror.Wrap(resp.JSON401, ErrCreateChannel, resp.StatusCode())
145+
default:
136146
c.logger.Error("Unexpected status code when creating channel",
137147
"status_code", resp.StatusCode(),
138148
"body", string(resp.Body))
139-
return nil, fmt.Errorf("%w: %w (status code %d)", ErrCreateChannel, ErrUnexpectedStatusCode, resp.StatusCode())
149+
return nil, fmt.Errorf("%w: %w (status code %d)", ErrCreateChannel, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
140150
}
141-
142-
if resp.JSON201 == nil {
143-
return nil, fmt.Errorf("%w: %w", ErrCreateChannel, ErrNilResponseBody)
144-
}
145-
146-
c.logger.Info("Channel created successfully",
147-
"channel_id", resp.JSON201.ChannelId.String(),
148-
"name", resp.JSON201.Name)
149-
150-
return resp.JSON201, nil
151151
}
152152

153153
// Get retrieves a specific channel by its ID.
@@ -167,30 +167,32 @@ func (c *Client) Get(ctx context.Context, channelID uuid.UUID) (*apiClient.Chann
167167
}
168168

169169
if resp == nil {
170-
return nil, fmt.Errorf("%w: %w", ErrGetChannel, ErrNilResponse)
171-
}
172-
173-
if resp.StatusCode() == 404 {
170+
return nil, fmt.Errorf("%w: %w", ErrGetChannel, apierror.ErrNilResponse)
171+
}
172+
173+
switch resp.StatusCode() {
174+
case http.StatusOK:
175+
if resp.JSON200 == nil {
176+
return nil, fmt.Errorf("%w: %w", ErrGetChannel, apierror.ErrNilResponseBody)
177+
}
178+
c.logger.Debug("Channel retrieved successfully",
179+
"channel_id", resp.JSON200.ChannelId.String(),
180+
"name", resp.JSON200.Name)
181+
return resp.JSON200, nil
182+
case http.StatusNotFound:
174183
c.logger.Warn("Channel not found", "channel_id", channelID.String())
175184
return nil, fmt.Errorf("%w: channel ID %s", ErrChannelNotFound, channelID.String())
176-
}
177-
178-
if resp.StatusCode() != 200 {
185+
case http.StatusUnauthorized:
186+
c.logger.Error("Unauthorized when getting channel",
187+
"status_code", resp.StatusCode(),
188+
"body", string(resp.Body))
189+
return nil, apierror.Wrap(resp.JSON401, ErrGetChannel, resp.StatusCode())
190+
default:
179191
c.logger.Error("Unexpected status code when getting channel",
180192
"status_code", resp.StatusCode(),
181193
"body", string(resp.Body))
182-
return nil, fmt.Errorf("%w: %w (status code %d)", ErrGetChannel, ErrUnexpectedStatusCode, resp.StatusCode())
183-
}
184-
185-
if resp.JSON200 == nil {
186-
return nil, fmt.Errorf("%w: %w", ErrGetChannel, ErrNilResponseBody)
194+
return nil, fmt.Errorf("%w: %w (status code %d)", ErrGetChannel, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
187195
}
188-
189-
c.logger.Debug("Channel retrieved successfully",
190-
"channel_id", resp.JSON200.ChannelId.String(),
191-
"name", resp.JSON200.Name)
192-
193-
return resp.JSON200, nil
194196
}
195197

196198
// ListInput defines the input parameters for listing channels.
@@ -229,25 +231,29 @@ func (c *Client) List(ctx context.Context, input ListInput) ([]apiClient.Channel
229231
}
230232

231233
if resp == nil {
232-
return nil, false, fmt.Errorf("%w: %w", ErrListChannels, ErrNilResponse)
233-
}
234-
235-
if resp.StatusCode() != 200 {
234+
return nil, false, fmt.Errorf("%w: %w", ErrListChannels, apierror.ErrNilResponse)
235+
}
236+
237+
switch resp.StatusCode() {
238+
case http.StatusOK:
239+
if resp.JSON200 == nil {
240+
return nil, false, fmt.Errorf("%w: %w", ErrListChannels, apierror.ErrNilResponseBody)
241+
}
242+
c.logger.Debug("Channels listed successfully",
243+
"count", len(resp.JSON200.Data),
244+
"has_more", resp.JSON200.HasMore)
245+
return resp.JSON200.Data, resp.JSON200.HasMore, nil
246+
case http.StatusUnauthorized:
247+
c.logger.Error("Unauthorized when listing channels",
248+
"status_code", resp.StatusCode(),
249+
"body", string(resp.Body))
250+
return nil, false, apierror.Wrap(resp.JSON401, ErrListChannels, resp.StatusCode())
251+
default:
236252
c.logger.Error("Unexpected status code when listing channels",
237253
"status_code", resp.StatusCode(),
238254
"body", string(resp.Body))
239-
return nil, false, fmt.Errorf("%w: %w (status code %d)", ErrListChannels, ErrUnexpectedStatusCode, resp.StatusCode())
240-
}
241-
242-
if resp.JSON200 == nil {
243-
return nil, false, fmt.Errorf("%w: %w", ErrListChannels, ErrNilResponseBody)
255+
return nil, false, fmt.Errorf("%w: %w (status code %d)", ErrListChannels, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
244256
}
245-
246-
c.logger.Debug("Channels listed successfully",
247-
"count", len(resp.JSON200.Data),
248-
"has_more", resp.JSON200.HasMore)
249-
250-
return resp.JSON200.Data, resp.JSON200.HasMore, nil
251257
}
252258

253259
// UpdateInput defines the input parameters for updating a channel.
@@ -285,30 +291,32 @@ func (c *Client) Update(ctx context.Context, channelID uuid.UUID, input UpdateIn
285291
}
286292

287293
if resp == nil {
288-
return nil, fmt.Errorf("%w: %w", ErrUpdateChannel, ErrNilResponse)
289-
}
290-
291-
if resp.StatusCode() == 404 {
294+
return nil, fmt.Errorf("%w: %w", ErrUpdateChannel, apierror.ErrNilResponse)
295+
}
296+
297+
switch resp.StatusCode() {
298+
case http.StatusOK:
299+
if resp.JSON200 == nil {
300+
return nil, fmt.Errorf("%w: %w", ErrUpdateChannel, apierror.ErrNilResponseBody)
301+
}
302+
c.logger.Info("Channel updated successfully",
303+
"channel_id", resp.JSON200.ChannelId.String(),
304+
"name", resp.JSON200.Name)
305+
return resp.JSON200, nil
306+
case http.StatusNotFound:
292307
c.logger.Warn("Channel not found", "channel_id", channelID.String())
293308
return nil, fmt.Errorf("%w: channel ID %s", ErrChannelNotFound, channelID.String())
294-
}
295-
296-
if resp.StatusCode() != 200 {
309+
case http.StatusUnauthorized:
310+
c.logger.Error("Unauthorized when updating channel",
311+
"status_code", resp.StatusCode(),
312+
"body", string(resp.Body))
313+
return nil, apierror.Wrap(resp.JSON401, ErrUpdateChannel, resp.StatusCode())
314+
default:
297315
c.logger.Error("Unexpected status code when updating channel",
298316
"status_code", resp.StatusCode(),
299317
"body", string(resp.Body))
300-
return nil, fmt.Errorf("%w: %w (status code %d)", ErrUpdateChannel, ErrUnexpectedStatusCode, resp.StatusCode())
318+
return nil, fmt.Errorf("%w: %w (status code %d)", ErrUpdateChannel, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
301319
}
302-
303-
if resp.JSON200 == nil {
304-
return nil, fmt.Errorf("%w: %w", ErrUpdateChannel, ErrNilResponseBody)
305-
}
306-
307-
c.logger.Info("Channel updated successfully",
308-
"channel_id", resp.JSON200.ChannelId.String(),
309-
"name", resp.JSON200.Name)
310-
311-
return resp.JSON200, nil
312320
}
313321

314322
// Archive archives a channel by transitioning it to archived status via PATCH.
@@ -335,26 +343,28 @@ func (c *Client) Archive(ctx context.Context, channelID uuid.UUID) (*apiClient.C
335343
}
336344

337345
if resp == nil {
338-
return nil, fmt.Errorf("%w: %w", ErrArchiveChannel, ErrNilResponse)
346+
return nil, fmt.Errorf("%w: %w", ErrArchiveChannel, apierror.ErrNilResponse)
339347
}
340348

341-
if resp.StatusCode() == 404 {
349+
switch resp.StatusCode() {
350+
case http.StatusOK:
351+
if resp.JSON200 == nil {
352+
return nil, fmt.Errorf("%w: %w", ErrArchiveChannel, apierror.ErrNilResponseBody)
353+
}
354+
c.logger.Info("Channel archived successfully", "channel_id", channelID.String())
355+
return resp.JSON200, nil
356+
case http.StatusNotFound:
342357
c.logger.Warn("Channel not found", "channel_id", channelID.String())
343358
return nil, fmt.Errorf("%w: channel ID %s", ErrChannelNotFound, channelID.String())
344-
}
345-
346-
if resp.StatusCode() != 200 {
359+
case http.StatusUnauthorized:
360+
c.logger.Error("Unauthorized when archiving channel",
361+
"status_code", resp.StatusCode(),
362+
"body", string(resp.Body))
363+
return nil, apierror.Wrap(resp.JSON401, ErrArchiveChannel, resp.StatusCode())
364+
default:
347365
c.logger.Error("Unexpected status code when archiving channel",
348366
"status_code", resp.StatusCode(),
349367
"body", string(resp.Body))
350-
return nil, fmt.Errorf("%w: %w (status code %d)", ErrArchiveChannel, ErrUnexpectedStatusCode, resp.StatusCode())
351-
}
352-
353-
if resp.JSON200 == nil {
354-
return nil, fmt.Errorf("%w: %w", ErrArchiveChannel, ErrNilResponseBody)
368+
return nil, fmt.Errorf("%w: %w (status code %d)", ErrArchiveChannel, apierror.ErrUnexpectedStatusCode, resp.StatusCode())
355369
}
356-
357-
c.logger.Info("Channel archived successfully", "channel_id", channelID.String())
358-
359-
return resp.JSON200, nil
360370
}

0 commit comments

Comments
 (0)