Skip to content

Commit 1bbb1c7

Browse files
fix(errors): return actionable errors from API (#13)
1 parent 70f7e86 commit 1bbb1c7

5 files changed

Lines changed: 446 additions & 46 deletions

File tree

auth/auth_test.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package auth
22

33
import (
4-
"errors"
54
"fmt"
65
"io/ioutil"
76
"net/http"
87
"net/http/httptest"
9-
"reflect"
108
"testing"
119

1210
deis "github.com/deis/controller-sdk-go"
@@ -247,17 +245,16 @@ func TestDeleteUserApp(t *testing.T) {
247245
server := httptest.NewServer(&handler)
248246
defer server.Close()
249247

250-
deis, err := deis.New(false, server.URL, "abc", "")
248+
d, err := deis.New(false, server.URL, "abc", "")
251249
if err != nil {
252250
t.Fatal(err)
253251
}
254252

255-
err = Delete(deis, "admin")
253+
err = Delete(d, "admin")
256254
// should be a 409 Conflict
257255

258-
expected := errors.New("409 Conflict")
259-
if reflect.DeepEqual(err, expected) == false {
260-
t.Errorf("got '%s' but expected '%s'", err, expected)
256+
if err != deis.ErrConflict {
257+
t.Errorf("got '%s' but expected '%s'", err, deis.ErrConflict)
261258
}
262259
}
263260

errors.go

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
package deis
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"io/ioutil"
9+
"net/http"
10+
"strings"
11+
)
12+
13+
const (
14+
// formatErrUnknown is used to create an dynamic error if no error matches
15+
formatErrUnknown = "Unknown Error (%d): %s"
16+
// fieldReqMsg is API error stating a field is required.
17+
fieldReqMsg = "This field is required."
18+
invalidUserMsg = "Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters."
19+
failedLoginMsg = "Unable to log in with provided credentials."
20+
invalidAppNameMsg = "App name can only contain a-z (lowercase), 0-9 and hypens"
21+
invalidNameMsg = "Can only contain a-z (lowercase), 0-9 and hypens"
22+
invalidCertMsg = "Could not load certificate"
23+
invalidPodMsg = "does not exist in application"
24+
invalidDomainMsg = "Hostname does not look valid."
25+
invalidVersionMsg = "version cannot be below 0"
26+
invalidKeyMsg = "Key contains invalid base64 chars"
27+
)
28+
29+
var (
30+
// ErrNotFound is returned when the server returns a 404.
31+
ErrNotFound = errors.New("Not Found")
32+
// ErrServerError is returned when the server returns a 500.
33+
ErrServerError = errors.New("Internal Server Error")
34+
// ErrMethodNotAllowed is thrown when using a unsupposrted method.
35+
// This should not come up unless there in an bug in the SDK.
36+
ErrMethodNotAllowed = errors.New("Method Not Allowed")
37+
// ErrInvalidUsername is returned when the user specifies an invalid or missing username.
38+
ErrInvalidUsername = errors.New(invalidUserMsg)
39+
// ErrMissingPassword is returned when a password is not sent with the request.
40+
ErrMissingPassword = errors.New("A Password is required")
41+
// ErrLogin is returned when the api cannot login fails with provided username and password
42+
ErrLogin = errors.New(failedLoginMsg)
43+
// ErrUnauthorized is given when the API returns a 401.
44+
ErrUnauthorized = errors.New("Unauthorized: Missing or Invalid Token")
45+
// ErrInvalidAppName is returned when the user specifies an invalid app name.
46+
ErrInvalidAppName = errors.New(invalidAppNameMsg)
47+
// ErrConflict is returned when the API returns a 409.
48+
ErrConflict = errors.New("This action could not be completed due to a conflict.")
49+
// ErrForbidden is returned when the API returns a 403.
50+
ErrForbidden = errors.New("You do not have permission to perform this action.")
51+
// ErrMissingKey is returned when a key is not sent with the request.
52+
ErrMissingKey = errors.New("A key is required")
53+
// ErrInvalidName is returned when a name is invalid or missing.
54+
ErrInvalidName = errors.New(invalidNameMsg)
55+
// ErrInvalidCertificate is returned when a certififate is missing or invalid
56+
ErrInvalidCertificate = errors.New(invalidCertMsg)
57+
// ErrPodNotFound is returned when a pod type is not Found
58+
ErrPodNotFound = errors.New("Pod not found in application")
59+
// ErrInvalidDomain is returned when a domain is missing or invalid
60+
ErrInvalidDomain = errors.New(invalidDomainMsg)
61+
// ErrInvalidImage is returned when a image is missing or invalid
62+
ErrInvalidImage = errors.New("The given image is invalid")
63+
// ErrInvalidVersion is returned when a version is invalid
64+
ErrInvalidVersion = errors.New("The given version is invalid")
65+
// ErrMissingID is returned when a ID is missing
66+
ErrMissingID = errors.New("An id is required")
67+
)
68+
69+
func checkForErrors(res *http.Response) error {
70+
if res.StatusCode >= 200 && res.StatusCode < 400 {
71+
return nil
72+
}
73+
74+
// Fix json.Decoder bug in <go1.7
75+
defer func() {
76+
io.Copy(ioutil.Discard, res.Body)
77+
res.Body.Close()
78+
}()
79+
80+
switch res.StatusCode {
81+
case 400:
82+
bodyMap := make(map[string]interface{})
83+
if err := json.NewDecoder(res.Body).Decode(&bodyMap); err != nil {
84+
return unknownError(res.StatusCode, err)
85+
}
86+
87+
if scanResponse(bodyMap, "username", []string{fieldReqMsg, invalidUserMsg}, true) {
88+
return ErrInvalidUsername
89+
}
90+
91+
if scanResponse(bodyMap, "password", []string{fieldReqMsg}, true) {
92+
return ErrMissingPassword
93+
}
94+
95+
if scanResponse(bodyMap, "non_field_errors", []string{failedLoginMsg}, true) {
96+
return ErrLogin
97+
}
98+
99+
if scanResponse(bodyMap, "id", []string{invalidAppNameMsg}, true) {
100+
return ErrInvalidAppName
101+
}
102+
103+
if scanResponse(bodyMap, "key", []string{fieldReqMsg}, true) {
104+
return ErrMissingKey
105+
}
106+
107+
if scanResponse(bodyMap, "public", []string{fieldReqMsg, invalidKeyMsg}, true) {
108+
return ErrMissingKey
109+
}
110+
111+
if scanResponse(bodyMap, "certificate", []string{fieldReqMsg, invalidCertMsg}, false) {
112+
return ErrInvalidCertificate
113+
}
114+
115+
if scanResponse(bodyMap, "name", []string{fieldReqMsg, invalidNameMsg}, true) {
116+
return ErrInvalidName
117+
}
118+
119+
if scanResponse(bodyMap, "domain", []string{invalidDomainMsg}, true) {
120+
return ErrInvalidDomain
121+
}
122+
123+
if scanResponse(bodyMap, "image", []string{fieldReqMsg}, true) {
124+
return ErrInvalidImage
125+
}
126+
127+
if scanResponse(bodyMap, "id", []string{fieldReqMsg}, true) {
128+
return ErrMissingID
129+
}
130+
131+
if v, ok := bodyMap["detail"].(string); ok {
132+
if strings.Contains(v, invalidPodMsg) {
133+
return ErrPodNotFound
134+
}
135+
if strings.Contains(v, invalidVersionMsg) {
136+
return ErrInvalidVersion
137+
}
138+
}
139+
140+
return unknownError(res.StatusCode, bodyMap)
141+
case 401:
142+
return ErrUnauthorized
143+
case 403:
144+
return ErrForbidden
145+
case 404:
146+
return ErrNotFound
147+
case 405:
148+
return ErrMethodNotAllowed
149+
case 409:
150+
return ErrConflict
151+
case 500:
152+
return ErrServerError
153+
default:
154+
out, err := ioutil.ReadAll(res.Body)
155+
if err != nil {
156+
return unknownError(res.StatusCode, err)
157+
}
158+
return unknownError(res.StatusCode, out)
159+
}
160+
}
161+
162+
func arrayContents(m map[string]interface{}, field string) []string {
163+
if v, ok := m[field]; ok {
164+
if a, ok := v.([]interface{}); ok {
165+
sa := []string{}
166+
167+
for _, i := range a {
168+
if s, ok := i.(string); ok {
169+
sa = append(sa, s)
170+
}
171+
}
172+
return sa
173+
}
174+
}
175+
176+
return []string{}
177+
}
178+
179+
func arrayContains(search string, completeMatch bool, array []string) bool {
180+
for _, element := range array {
181+
if completeMatch {
182+
if element == search {
183+
return true
184+
}
185+
} else {
186+
if strings.Contains(element, search) {
187+
return true
188+
}
189+
}
190+
}
191+
192+
return false
193+
}
194+
195+
func unknownError(sc int, k interface{}) error {
196+
return fmt.Errorf(formatErrUnknown, sc, k)
197+
}
198+
199+
func scanResponse(
200+
body map[string]interface{}, field string, errMsgs []string, completeMatch bool) bool {
201+
for _, msg := range errMsgs {
202+
if arrayContains(msg, completeMatch, arrayContents(body, field)) {
203+
return true
204+
}
205+
}
206+
207+
return false
208+
}

0 commit comments

Comments
 (0)