Make all tests that deal with the store interfaces go through the Context. This
is mainly important so that pre- and post- save/retrieval/deletion/whatever
transforms can be done without doing them in every single implementation of the
store.
Change the Endpoint URI property to be a string, not a *url.URL. This makes
testing easier, JSON responses cleaner, and is all around just a better
strategy. Just because we turn it into a URL every now and then doesn't mean
that's how we need to store it.
Add JSON tags to the Client type and Endpoint type.
Create normalizeURI and normalizeURIString methods to... well, normalize the
Endpoint URIs. This makes it so that we can compare them, and forgive some
arbitrary user behaviour (like slashes, etc.)
Add a NormalizedURI property to the Endpoint type. This is where we store the
NormalizedURI, which is what we'll be using when we want to check if an endpoint
is valid or not. For the sake of tests and predictability, however, we always
want to redirect to the URI, not the NormalizedURI.
Add checks to the Client creation API endpoint to give better errors. Now
leaving out the Type won't be considered an invalid type, it will be considered
a missing parameter. An empty name will be reported as a missing parameter, a
name with too few characters will be reported as an insufficient name, and a
name with too many characters will be reported as an overflow name. We gather as
many of these errors as apply before returning.
Check if an Endpoint URI is absolute before adding it as an endpoint, or return
an invalid value error if it is not.
Always return the errors array when creating a client. We could succeed in
creating one or more things and still have errors. We should return anything
that's created _as well as_ any errors encountered.
Add unit testing for our CreateClientHandler.
Fix our oauth2 tests so that if there's an error in the body, it's in the test
logs. This should help debugging significantly.
Fix our oauth2 tests so that the Profile only requires 1 iteration for its
password hashing. This means each time we want to validate a session, it doesn't
add a full second to our test runs. This is a big speed improvement for our
tests.
Add test helper methods for comparing API errors, API responses, and filling in
server-generated information in a response that it's impossible to have an
expectation around (e.g., IDs) so that we can use our comparison helpers to
check if a response is as we expect it.
Fix a typo in our Context helpers that was reporting no sessionStore being set
_only_ when a sessionStore was set. So yes, the opposite of what we wanted.
Oops. This was discovered by passing all our tests through the context. methods
instead of operating on the stores themselves.
13 "code.secondbit.org/uuid.hg"
16 var authCodeStores = []authorizationCodeStore{NewMemstore()}
18 func compareAuthorizationCodes(authCode1, authCode2 AuthorizationCode) (success bool, field string, authCode1val, authCode2val interface{}) {
19 if authCode1.Code != authCode2.Code {
20 return false, "code", authCode1.Code, authCode2.Code
22 if !authCode1.Created.Equal(authCode2.Created) {
23 return false, "created", authCode1.Created, authCode2.Created
25 if authCode1.ExpiresIn != authCode2.ExpiresIn {
26 return false, "expires in", authCode1.ExpiresIn, authCode2.ExpiresIn
28 if !authCode1.ClientID.Equal(authCode2.ClientID) {
29 return false, "client ID", authCode1.ClientID, authCode2.ClientID
31 if authCode1.Scope != authCode2.Scope {
32 return false, "scope", authCode1.Scope, authCode2.Scope
34 if authCode1.RedirectURI != authCode2.RedirectURI {
35 return false, "redirect URI", authCode1.RedirectURI, authCode2.RedirectURI
37 if authCode1.State != authCode2.State {
38 return false, "state", authCode1.State, authCode2.State
40 if !authCode1.ProfileID.Equal(authCode2.ProfileID) {
41 return false, "profile ID", authCode1.ProfileID, authCode2.ProfileID
43 if authCode1.Used != authCode2.Used {
44 return false, "used", authCode1.Used, authCode2.Used
46 return true, "", nil, nil
49 func TestAuthorizationCodeStore(t *testing.T) {
51 authCode := AuthorizationCode{
55 ClientID: uuid.NewID(),
57 RedirectURI: "redirectURI",
60 for _, store := range authCodeStores {
61 context := Context{authCodes: store}
62 err := context.SaveAuthorizationCode(authCode)
64 t.Errorf("Error saving auth code to %T: %s", store, err)
66 err = context.SaveAuthorizationCode(authCode)
67 if err != ErrAuthorizationCodeAlreadyExists {
68 t.Errorf("Expected ErrAuthorizationCodeAlreadyExists from %T, got %+v", store, err)
70 retrieved, err := context.GetAuthorizationCode(authCode.Code)
72 t.Errorf("Error retrieving auth code from %T: %s", store, err)
74 match, field, expectation, result := compareAuthorizationCodes(authCode, retrieved)
76 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
78 err = context.UseAuthorizationCode(authCode.Code)
80 t.Errorf("Error retrieving auth code from %T: %s", store, err)
82 retrieved, err = context.GetAuthorizationCode(authCode.Code)
84 t.Errorf("Error retrieving auth code from %T: %s", store, err)
87 match, field, expectation, result = compareAuthorizationCodes(authCode, retrieved)
89 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
91 err = context.DeleteAuthorizationCode(authCode.Code)
93 t.Errorf("Error removing auth code from %T: %s", store, err)
95 retrieved, err = context.GetAuthorizationCode(authCode.Code)
96 if err != ErrAuthorizationCodeNotFound {
97 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v and %+v", store, retrieved, err)
99 err = context.DeleteAuthorizationCode(authCode.Code)
100 if err != ErrAuthorizationCodeNotFound {
101 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
103 err = context.UseAuthorizationCode(authCode.Code)
104 if err != ErrAuthorizationCodeNotFound {
105 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
110 func TestAuthCodeGrantValidate(t *testing.T) {
112 store := NewMemstore()
113 testContext := Context{
122 Secret: "super secret!",
123 OwnerID: uuid.NewID(),
124 Name: "My test client",
125 Logo: "https://secondbit.org/logo.png",
126 Website: "https://secondbit.org/",
129 endpoint := Endpoint{
132 URI: "https://test.secondbit.org/redirect",
135 err := testContext.SaveClient(client)
137 t.Fatal("Can't store client:", err)
139 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
141 t.Fatal("Can't store endpoint:", err)
143 code := AuthorizationCode{
147 ClientID: uuid.NewID(),
149 RedirectURI: "redirectURI",
152 err = testContext.SaveAuthorizationCode(code)
154 t.Fatal("Can't add auth code:", err)
157 code2.Code = "otherauthcode"
158 code2.ClientID = client.ID
159 err = testContext.SaveAuthorizationCode(code2)
161 t.Fatal("Can't add second auth code:", err)
163 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
165 t.Fatal("Can't build request:", err)
167 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
168 w := httptest.NewRecorder()
169 params := url.Values{}
170 body := bytes.NewBufferString(params.Encode())
171 req.Body = ioutil.NopCloser(body)
172 scope, profileID, valid := authCodeGrantValidate(w, req, testContext)
174 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
176 if w.Code != http.StatusBadRequest {
177 t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
179 expectedBody := `{"error":"invalid_request"}`
180 if strings.TrimSpace(w.Body.String()) != expectedBody {
181 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
184 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
186 t.Fatal("Can't build request:", err)
188 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
189 w = httptest.NewRecorder()
190 params = url.Values{}
191 params.Set("code", "notmycode")
192 body = bytes.NewBufferString(params.Encode())
193 req.Body = ioutil.NopCloser(body)
194 err = req.ParseForm()
198 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
200 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
202 if w.Code != http.StatusUnauthorized {
203 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
205 expectedBody = `{"error":"invalid_client"}`
206 if expectedBody != strings.TrimSpace(w.Body.String()) {
207 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
210 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
212 t.Fatal("Can't build request:", err)
214 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
215 req.SetBasicAuth(client.ID.String(), client.Secret)
216 w = httptest.NewRecorder()
217 params = url.Values{}
218 params.Set("code", "notmycode")
219 body = bytes.NewBufferString(params.Encode())
220 req.Body = ioutil.NopCloser(body)
221 err = req.ParseForm()
225 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
227 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
229 if w.Code != http.StatusBadRequest {
230 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
232 expectedBody = `{"error":"invalid_grant"}`
233 if expectedBody != strings.TrimSpace(w.Body.String()) {
234 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
237 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
239 t.Fatal("Can't build request:", err)
241 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
242 req.SetBasicAuth(client.ID.String(), client.Secret)
243 w = httptest.NewRecorder()
244 params = url.Values{}
245 params.Set("code", code.Code)
246 params.Set("redirect_uri", "not my redirectURI")
247 body = bytes.NewBufferString(params.Encode())
248 req.Body = ioutil.NopCloser(body)
249 err = req.ParseForm()
253 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
255 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
257 if w.Code != http.StatusBadRequest {
258 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
260 expectedBody = `{"error":"invalid_grant"}`
261 if expectedBody != strings.TrimSpace(w.Body.String()) {
262 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
265 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
267 t.Fatal("Can't build request:", err)
269 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
270 req.SetBasicAuth(client.ID.String(), client.Secret)
271 w = httptest.NewRecorder()
272 params = url.Values{}
273 params.Set("code", code.Code)
274 params.Set("redirect_uri", code.RedirectURI)
275 body = bytes.NewBufferString(params.Encode())
276 req.Body = ioutil.NopCloser(body)
277 err = req.ParseForm()
281 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
283 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
285 if w.Code != http.StatusBadRequest {
286 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
288 expectedBody = `{"error":"invalid_grant"}`
289 if expectedBody != strings.TrimSpace(w.Body.String()) {
290 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
293 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
295 t.Fatal("Can't build request:", err)
297 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
298 req.SetBasicAuth(client.ID.String(), client.Secret)
299 w = httptest.NewRecorder()
300 params = url.Values{}
301 params.Set("code", code2.Code)
302 params.Set("redirect_uri", code2.RedirectURI)
303 body = bytes.NewBufferString(params.Encode())
304 req.Body = ioutil.NopCloser(body)
305 err = req.ParseForm()
309 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
311 t.Fatalf("Expected valid auth code, was not valid.")
315 func TestAuthCodeGrantInvalidate(t *testing.T) {
317 store := NewMemstore()
318 testContext := Context{
325 code := AuthorizationCode{
329 ClientID: uuid.NewID(),
331 RedirectURI: "redirectURI",
334 err := testContext.SaveAuthorizationCode(code)
336 t.Fatal("Can't add auth code:", err)
338 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
340 t.Fatal("Can't build request:", err)
342 err = authCodeGrantInvalidate(req, testContext)
343 if err != ErrAuthorizationCodeNotFound {
344 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
346 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
348 t.Fatal("Can't build request:", err)
350 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
351 params := url.Values{}
352 params.Set("code", "notmycode")
353 body := bytes.NewBufferString(params.Encode())
354 req.Body = ioutil.NopCloser(body)
355 err = authCodeGrantInvalidate(req, testContext)
356 if err != ErrAuthorizationCodeNotFound {
357 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
359 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
361 t.Fatal("Can't build request:", err)
363 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
364 params.Set("code", code.Code)
365 body = bytes.NewBufferString(params.Encode())
366 req.Body = ioutil.NopCloser(body)
367 err = authCodeGrantInvalidate(req, testContext)
369 t.Error("Error invalidating auth code:", err)
371 authCode, err := testContext.GetAuthorizationCode(code.Code)
373 t.Error("Error retrieving auth code:", err)
376 t.Error("Expected auth code to be used, was not.")