Test our Postgres profileStore implementation.
Update all our test cases to use time.Now().Round(time.Millisecond), because Go
uses nanosecond precision on time values, but Postgres silently truncates that
to millisecond precision. This caused our tests to report false failures that
were just silent precision loss, not actual failures.
Set up our authd server to use the Postgres store for profiles and automatically
create a test scope when starting up.
Log errors when creating Clients through the API, instead of just swallowing
them and sending back cryptic act of god errors.
Add a NewPostgres helper that returns a postgres profileStore from a connection
string (passed through pq transparently).
Add an Empty() bool helper to ProfileChange and BulkProfileChange types, so we
can determine if there are any changes we need to act on easily.
Log errors when creating Pofiles through the API, instead of just swalloing them
and sending back cryptic act of god errors.
Remove the ` quotes around field and table names, which are not supported in
Postgres. This required adding a few functions/methods to pan.
Detect situations where a profile was expected and not found, and return
ErrProfileNotFound.
Detect pq errors thrown when the profiles_pkey constraint is violated, and
transform them to the ErrProfileAlreadyExists error.
Detect empty ProfileChange and BulkProfileChange variables and abort the
updateProfile and updateProfiles methods early, before invalid SQL is generated.
Detect pq errors thrown when the logins_pkey constraint is violated, and
transform them to the ErrLoginAlreadyExists error.
Detect when removing a Login and no rows were affected, and return an
ErrLoginNotFound.
Create an sql dir with a postgres_init script that will initialize the schema of
the tables expected in the database.
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 len(authCode1.Scopes) != len(authCode2.Scopes) {
32 return false, "scopes", authCode1.Scopes, authCode2.Scopes
34 for pos, scope := range authCode1.Scopes {
35 if scope != authCode2.Scopes[pos] {
36 return false, "scopes", authCode1.Scopes, authCode2.Scopes
39 if authCode1.RedirectURI != authCode2.RedirectURI {
40 return false, "redirect URI", authCode1.RedirectURI, authCode2.RedirectURI
42 if authCode1.State != authCode2.State {
43 return false, "state", authCode1.State, authCode2.State
45 if !authCode1.ProfileID.Equal(authCode2.ProfileID) {
46 return false, "profile ID", authCode1.ProfileID, authCode2.ProfileID
48 if authCode1.Used != authCode2.Used {
49 return false, "used", authCode1.Used, authCode2.Used
51 return true, "", nil, nil
54 func TestAuthorizationCodeStore(t *testing.T) {
56 authCode := AuthorizationCode{
58 Created: time.Now().Round(time.Millisecond),
60 ClientID: uuid.NewID(),
61 Scopes: []string{"scope"},
62 RedirectURI: "redirectURI",
65 for _, store := range authCodeStores {
66 context := Context{authCodes: store}
67 err := context.SaveAuthorizationCode(authCode)
69 t.Errorf("Error saving auth code to %T: %s", store, err)
71 err = context.SaveAuthorizationCode(authCode)
72 if err != ErrAuthorizationCodeAlreadyExists {
73 t.Errorf("Expected ErrAuthorizationCodeAlreadyExists from %T, got %+v", store, err)
75 retrieved, err := context.GetAuthorizationCode(authCode.Code)
77 t.Errorf("Error retrieving auth code from %T: %s", store, err)
79 match, field, expectation, result := compareAuthorizationCodes(authCode, retrieved)
81 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
83 err = context.UseAuthorizationCode(authCode.Code)
85 t.Errorf("Error retrieving auth code from %T: %s", store, err)
87 retrieved, err = context.GetAuthorizationCode(authCode.Code)
89 t.Errorf("Error retrieving auth code from %T: %s", store, err)
92 match, field, expectation, result = compareAuthorizationCodes(authCode, retrieved)
94 t.Errorf("Expected `%v` in the `%s` field of auth code retrieved from %T, got `%v`", expectation, field, store, result)
96 err = context.DeleteAuthorizationCode(authCode.Code)
98 t.Errorf("Error removing auth code from %T: %s", store, err)
100 retrieved, err = context.GetAuthorizationCode(authCode.Code)
101 if err != ErrAuthorizationCodeNotFound {
102 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v and %+v", store, retrieved, err)
104 err = context.DeleteAuthorizationCode(authCode.Code)
105 if err != ErrAuthorizationCodeNotFound {
106 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
108 err = context.UseAuthorizationCode(authCode.Code)
109 if err != ErrAuthorizationCodeNotFound {
110 t.Errorf("Expected ErrAuthorizationCodeNotFound from %T, got %+v", store, err)
115 func TestAuthCodeGrantValidate(t *testing.T) {
117 store := NewMemstore()
118 testContext := Context{
127 Secret: "super secret!",
128 OwnerID: uuid.NewID(),
129 Name: "My test client",
130 Logo: "https://secondbit.org/logo.png",
131 Website: "https://secondbit.org/",
134 endpoint := Endpoint{
137 URI: "https://test.secondbit.org/redirect",
138 Added: time.Now().Round(time.Millisecond),
140 err := testContext.SaveClient(client)
142 t.Fatal("Can't store client:", err)
144 err = testContext.AddEndpoints(client.ID, []Endpoint{endpoint})
146 t.Fatal("Can't store endpoint:", err)
148 code := AuthorizationCode{
150 Created: time.Now().Round(time.Millisecond),
152 ClientID: uuid.NewID(),
153 Scopes: []string{"scope"},
154 RedirectURI: "redirectURI",
157 err = testContext.SaveAuthorizationCode(code)
159 t.Fatal("Can't add auth code:", err)
162 code2.Code = "otherauthcode"
163 code2.ClientID = client.ID
164 err = testContext.SaveAuthorizationCode(code2)
166 t.Fatal("Can't add second auth code:", err)
168 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
170 t.Fatal("Can't build request:", err)
172 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
173 w := httptest.NewRecorder()
174 params := url.Values{}
175 body := bytes.NewBufferString(params.Encode())
176 req.Body = ioutil.NopCloser(body)
177 scope, profileID, valid := authCodeGrantValidate(w, req, testContext)
179 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
181 if w.Code != http.StatusBadRequest {
182 t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code)
184 expectedBody := `{"error":"invalid_request"}`
185 if strings.TrimSpace(w.Body.String()) != expectedBody {
186 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
189 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
191 t.Fatal("Can't build request:", err)
193 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
194 w = httptest.NewRecorder()
195 params = url.Values{}
196 params.Set("code", "notmycode")
197 body = bytes.NewBufferString(params.Encode())
198 req.Body = ioutil.NopCloser(body)
199 err = req.ParseForm()
203 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
205 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
207 if w.Code != http.StatusUnauthorized {
208 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
210 expectedBody = `{"error":"invalid_client"}`
211 if expectedBody != strings.TrimSpace(w.Body.String()) {
212 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
215 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
217 t.Fatal("Can't build request:", err)
219 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
220 req.SetBasicAuth(client.ID.String(), client.Secret)
221 w = httptest.NewRecorder()
222 params = url.Values{}
223 params.Set("code", "notmycode")
224 body = bytes.NewBufferString(params.Encode())
225 req.Body = ioutil.NopCloser(body)
226 err = req.ParseForm()
230 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
232 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
234 if w.Code != http.StatusBadRequest {
235 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
237 expectedBody = `{"error":"invalid_grant"}`
238 if expectedBody != strings.TrimSpace(w.Body.String()) {
239 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
242 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
244 t.Fatal("Can't build request:", err)
246 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
247 req.SetBasicAuth(client.ID.String(), client.Secret)
248 w = httptest.NewRecorder()
249 params = url.Values{}
250 params.Set("code", code.Code)
251 params.Set("redirect_uri", "not my redirectURI")
252 body = bytes.NewBufferString(params.Encode())
253 req.Body = ioutil.NopCloser(body)
254 err = req.ParseForm()
258 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
260 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
262 if w.Code != http.StatusBadRequest {
263 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
265 expectedBody = `{"error":"invalid_grant"}`
266 if expectedBody != strings.TrimSpace(w.Body.String()) {
267 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
270 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
272 t.Fatal("Can't build request:", err)
274 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
275 req.SetBasicAuth(client.ID.String(), client.Secret)
276 w = httptest.NewRecorder()
277 params = url.Values{}
278 params.Set("code", code.Code)
279 params.Set("redirect_uri", code.RedirectURI)
280 body = bytes.NewBufferString(params.Encode())
281 req.Body = ioutil.NopCloser(body)
282 err = req.ParseForm()
286 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
288 t.Fatalf("Expected invalid auth code, got scope `%s` and profileID `%s`.", scope, profileID)
290 if w.Code != http.StatusBadRequest {
291 t.Errorf("Expected status %d, got %d", http.StatusUnauthorized, w.Code)
293 expectedBody = `{"error":"invalid_grant"}`
294 if expectedBody != strings.TrimSpace(w.Body.String()) {
295 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
298 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
300 t.Fatal("Can't build request:", err)
302 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
303 req.SetBasicAuth(client.ID.String(), client.Secret)
304 w = httptest.NewRecorder()
305 params = url.Values{}
306 params.Set("code", code2.Code)
307 params.Set("redirect_uri", code2.RedirectURI)
308 body = bytes.NewBufferString(params.Encode())
309 req.Body = ioutil.NopCloser(body)
310 err = req.ParseForm()
314 scope, profileID, valid = authCodeGrantValidate(w, req, testContext)
316 t.Fatalf("Expected valid auth code, was not valid.")
320 func TestAuthCodeGrantInvalidate(t *testing.T) {
322 store := NewMemstore()
323 testContext := Context{
330 code := AuthorizationCode{
332 Created: time.Now().Round(time.Millisecond),
334 ClientID: uuid.NewID(),
335 Scopes: []string{"scope"},
336 RedirectURI: "redirectURI",
339 err := testContext.SaveAuthorizationCode(code)
341 t.Fatal("Can't add auth code:", err)
343 req, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
345 t.Fatal("Can't build request:", err)
347 err = authCodeGrantInvalidate(req, testContext)
348 if err != ErrAuthorizationCodeNotFound {
349 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
351 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
353 t.Fatal("Can't build request:", err)
355 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
356 params := url.Values{}
357 params.Set("code", "notmycode")
358 body := bytes.NewBufferString(params.Encode())
359 req.Body = ioutil.NopCloser(body)
360 err = authCodeGrantInvalidate(req, testContext)
361 if err != ErrAuthorizationCodeNotFound {
362 t.Errorf("Expected `%s`, got `%+v`", ErrAuthorizationCodeNotFound, err)
364 req, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
366 t.Fatal("Can't build request:", err)
368 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
369 params.Set("code", code.Code)
370 body = bytes.NewBufferString(params.Encode())
371 req.Body = ioutil.NopCloser(body)
372 err = authCodeGrantInvalidate(req, testContext)
374 t.Error("Error invalidating auth code:", err)
376 authCode, err := testContext.GetAuthorizationCode(code.Code)
378 t.Error("Error retrieving auth code:", err)
381 t.Error("Expected auth code to be used, was not.")