Test our GetClientHandler function, add isAuthError helper.
Add a helper that identifies whether the error passed to it is an authentication
error or is some other type of error. This is useful fo checking whether or not
an internal error occurred while authenticating users.
Update all instances where we call our authentication helper to make them use
the new error helper. All tests continue to pass.
Add a new test case for retrieving a client as an unauthenticated user. This
clears the client's secret from the response before sending it.
Update the GetClientHandler function to return the secret when the owner of the
client used Basic Auth in the request.
Add a new test case for retrieving a client as an authenticated user, both the
owner and a non-owner user. This makes sure the secret is divulged only in the
appropriate cases.
7 "github.com/gorilla/mux"
17 "code.secondbit.org/uuid.hg"
21 clientChangeSecret = 1 << iota
28 var clientStores = []clientStore{NewMemstore()}
30 func compareClients(client1, client2 Client) (success bool, field string, val1, val2 interface{}) {
31 if !client1.ID.Equal(client2.ID) {
32 return false, "ID", client1.ID, client2.ID
34 if client1.Secret != client2.Secret {
35 return false, "secret", client1.Secret, client2.Secret
37 if !client1.OwnerID.Equal(client2.OwnerID) {
38 return false, "owner ID", client1.OwnerID, client2.OwnerID
40 if client1.Name != client2.Name {
41 return false, "name", client1.Name, client2.Name
43 if client1.Logo != client2.Logo {
44 return false, "logo", client1.Logo, client2.Logo
46 if client1.Website != client2.Website {
47 return false, "website", client1.Website, client2.Website
49 if client1.Type != client2.Type {
50 return false, "type", client1.Type, client2.Type
52 return true, "", nil, nil
55 func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) {
56 if !endpoint1.ID.Equal(endpoint2.ID) {
57 return false, "ID", endpoint1.ID, endpoint2.ID
59 if !endpoint1.ClientID.Equal(endpoint2.ClientID) {
60 return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID
62 if !endpoint1.Added.Equal(endpoint2.Added) {
63 return false, "Added", endpoint1.Added, endpoint2.Added
65 if endpoint1.URI != endpoint2.URI {
66 return false, "URI", endpoint1.URI, endpoint2.URI
68 return true, "", nil, nil
71 func TestClientStoreSuccess(t *testing.T) {
76 OwnerID: uuid.NewID(),
81 for _, store := range clientStores {
82 context := Context{clients: store}
83 err := context.SaveClient(client)
85 t.Fatalf("Error saving client to %T: %s", store, err)
87 err = context.SaveClient(client)
88 if err != ErrClientAlreadyExists {
89 t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
91 retrieved, err := context.GetClient(client.ID)
93 t.Fatalf("Error retrieving client from %T: %s", store, err)
95 success, field, expectation, result := compareClients(client, retrieved)
97 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
99 clients, err := context.ListClientsByOwner(client.OwnerID, 25, 0)
101 t.Fatalf("Error retrieving clients by owner from %T: %s", store, err)
103 if len(clients) != 1 {
104 t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients)
106 success, field, expectation, result = compareClients(client, clients[0])
108 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
110 err = context.DeleteClient(client.ID)
112 t.Fatalf("Error deleting client from %T: %s", store, err)
114 err = context.DeleteClient(client.ID)
115 if err != ErrClientNotFound {
116 t.Fatalf("Expected ErrClientNotFound, got %s from %T", err, store)
118 retrieved, err = context.GetClient(client.ID)
119 if err != ErrClientNotFound {
120 t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
122 clients, err = context.ListClientsByOwner(client.OwnerID, 25, 0)
124 t.Fatalf("Error listing clients by owner from %T: %s", store, err)
126 if len(clients) != 0 {
127 t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients)
132 func TestEndpointStoreSuccess(t *testing.T) {
137 OwnerID: uuid.NewID(),
142 endpoint1 := Endpoint{
146 URI: "https://www.example.com/",
148 endpoint2 := Endpoint{
152 URI: "https://www.example.com/my/full/path",
154 for _, store := range clientStores {
155 context := Context{clients: store}
156 err := context.SaveClient(client)
158 t.Fatalf("Error saving client to %T: %s", store, err)
160 err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
162 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
164 endpoints, err := context.ListEndpoints(client.ID, 10, 0)
166 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
168 if len(endpoints) != 1 {
169 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
171 success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0])
173 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
175 err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
177 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
179 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
181 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
183 if len(endpoints) != 2 {
184 t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store)
186 sortedEnd := sortedEndpoints(endpoints)
188 endpoints = []Endpoint(sortedEnd)
189 success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0])
191 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
193 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1])
195 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
197 err = context.RemoveEndpoint(client.ID, endpoint1.ID)
199 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
201 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
203 t.Fatalf("Error listing endpoints in %T: %s", store, err)
205 if len(endpoints) != 1 {
206 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
208 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0])
210 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
212 err = context.RemoveEndpoint(client.ID, endpoint2.ID)
214 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
216 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
218 t.Fatalf("Error listing endpoints in %T: %s", store, err)
220 if len(endpoints) != 0 {
221 t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store)
226 func TestClientUpdates(t *testing.T) {
232 OwnerID: uuid.NewID(),
237 for i := 0; i < variations; i++ {
238 var secret, name, logo, website string
239 change := ClientChange{}
240 expectation := client
242 if i&clientChangeSecret != 0 {
243 secret = fmt.Sprintf("secret-%d", i)
244 change.Secret = &secret
245 expectation.Secret = secret
247 if i&clientChangeOwnerID != 0 {
248 change.OwnerID = uuid.NewID()
249 expectation.OwnerID = change.OwnerID
251 if i&clientChangeName != 0 {
252 name = fmt.Sprintf("name-%d", i)
254 expectation.Name = name
256 if i&clientChangeLogo != 0 {
257 logo = fmt.Sprintf("logo-%d", i)
259 expectation.Logo = logo
261 if i&clientChangeWebsite != 0 {
262 website = fmt.Sprintf("website-%d", i)
263 change.Website = &website
264 expectation.Website = website
266 result.ApplyChange(change)
267 match, field, expected, got := compareClients(expectation, result)
269 t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
271 for _, store := range clientStores {
272 context := Context{clients: store}
273 err := context.SaveClient(client)
275 t.Fatalf("Error saving client in %T: %s", store, err)
277 err = context.UpdateClient(client.ID, change)
279 t.Fatalf("Error updating client in %T: %s", store, err)
281 retrieved, err := context.GetClient(client.ID)
283 t.Fatalf("Error getting client from %T: %s", store, err)
285 match, field, expected, got = compareClients(expectation, retrieved)
287 t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
289 err = context.DeleteClient(client.ID)
291 t.Fatalf("Error deleting client from %T: %s", store, err)
293 err = context.UpdateClient(client.ID, change)
294 if err != ErrClientNotFound {
295 t.Fatalf("Expected ErrClientNotFound, got %v from %T", err, store)
301 func TestClientEndpointChecks(t *testing.T) {
306 OwnerID: uuid.NewID(),
311 endpoint1 := Endpoint{
315 URI: "https://www.example.com/first",
317 endpoint2 := Endpoint{
321 URI: "https://www.example.com/my/full/path",
323 candidates := map[string]bool{
324 "https://www.example.com/": false,
325 "https://www.example.com/first": true,
326 "https://www.example.com/first/extra/path": false,
327 "https://www.example.com/my": false,
328 "https://www.example.com/my/full/path": true,
330 for _, store := range clientStores {
331 context := Context{clients: store}
332 err := context.SaveClient(client)
334 t.Fatalf("Error saving client in %T: %s", store, err)
336 err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
338 t.Fatalf("Error saving endpoint in %T: %s", store, err)
340 err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
342 t.Fatalf("Error saving endpoint in %T: %s", store, err)
344 for candidate, expectation := range candidates {
345 result, err := context.CheckEndpoint(client.ID, candidate)
347 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
349 if result != expectation {
356 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
362 func TestClientEndpointChecksStrict(t *testing.T) {
367 OwnerID: uuid.NewID(),
372 endpoint1 := Endpoint{
376 URI: "https://www.example.com/first",
378 endpoint2 := Endpoint{
382 URI: "https://www.example.com/my/full/path",
384 candidates := map[string]bool{
385 "https://www.example.com/": false,
386 "https://www.example.com/first": true,
387 "https://www.example.com/first/extra/path": false,
388 "https://www.example.com/my": false,
389 "https://www.example.com/my/full/path": true,
391 for _, store := range clientStores {
392 context := Context{clients: store}
393 err := context.SaveClient(client)
395 t.Fatalf("Error saving client in %T: %s", store, err)
397 err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
399 t.Fatalf("Error saving endpoint in %T: %s", store, err)
401 err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
403 t.Fatalf("Error saving endpoint in %T: %s", store, err)
405 for candidate, expectation := range candidates {
406 result, err := context.CheckEndpoint(client.ID, candidate)
408 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
410 if result != expectation {
417 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
423 func TestClientChangeValidation(t *testing.T) {
425 change := ClientChange{}
426 if err := change.Validate(); err[0] != ErrEmptyChange {
427 t.Errorf("Expected %s to give an error of %s, gave %s", "empty change", ErrEmptyChange, err)
429 names := map[string][]error{
430 "a": []error{ErrClientNameTooShort},
433 "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": []error{ErrClientNameTooLong},
435 for name, expectation := range names {
436 change = ClientChange{Name: &name}
437 errs := change.Validate()
438 if len(errs) != len(expectation) {
439 t.Errorf("Expected %s to give %d errors, gave %d", name, len(expectation), len(errs))
442 for pos, err := range errs {
443 if err != expectation[pos] {
444 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", name, expectation[pos], pos, err)
449 for i := 0; i < 1025; i++ {
450 longPath = fmt.Sprintf("%s%d", longPath, i)
452 logos := map[string][]error{
453 "https://www.example.com/" + longPath: []error{ErrClientLogoTooLong},
454 "https://www.example.com/ab": []error{},
455 "www.example.com/ab": []error{ErrClientLogoNotURL},
456 "test": []error{ErrClientLogoNotURL},
459 for logo, expectation := range logos {
460 change = ClientChange{Logo: &logo}
461 errs := change.Validate()
462 if len(errs) != len(expectation) {
463 t.Errorf("Expected %s to give %d errors, gave %d", logo, len(expectation), len(errs))
465 for pos, err := range errs {
466 if err != expectation[pos] {
467 t.Errorf("Expected %s to give an error of %s in positiong %d, gave %s", logo, expectation[pos], pos, err)
471 websites := map[string][]error{
472 "https://www.example.com/" + longPath: []error{ErrClientWebsiteTooLong},
473 "https://www.example.com/ab": []error{},
474 "www.example.com/ab": []error{ErrClientWebsiteNotURL},
475 "test": []error{ErrClientWebsiteNotURL},
478 for website, expectation := range websites {
479 change = ClientChange{Website: &website}
480 errs := change.Validate()
481 if len(errs) != len(expectation) {
482 t.Errorf("Expected %s to give %d errors, gave %d", website, len(expectation), len(errs))
484 for pos, err := range errs {
485 if err != expectation[pos] {
486 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", website, expectation[pos], pos, err)
492 func TestGetClientAuth(t *testing.T) {
494 type clientAuthRequest struct {
499 expectedClientID uuid.ID
500 expectedClientSecret string
504 expectAuthenticateHeader bool
507 tests := []clientAuthRequest{
508 {"", "", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
509 {"", "", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
510 {"", "no clientID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
511 {"", "no clientID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
512 {"not an actual id", "invalid client ID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
513 {"not an actual id", "invalid client ID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
514 {"", "", "not an actual id", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
515 {id.String(), "secret", "", true, id, "secret", true, http.StatusOK, "", false},
516 {id.String(), "secret", "", false, id, "secret", true, http.StatusOK, "", false},
517 {"", "", id.String(), true, id, "", true, http.StatusOK, "", false},
518 {"", "", id.String(), false, nil, "", false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
520 for pos, test := range tests {
521 t.Logf("Running test #%d, with request %+v", pos, test)
522 w := httptest.NewRecorder()
523 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
525 t.Fatal("Can't build request:", err)
527 if test.username != "" || test.pass != "" {
528 r.SetBasicAuth(test.username, test.pass)
530 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
531 params := url.Values{}
532 params.Set("client_id", test.clientID)
533 body := bytes.NewBufferString(params.Encode())
534 r.Body = ioutil.NopCloser(body)
535 respID, respSecret, success := getClientAuth(w, r, test.allowPublic)
536 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
537 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
539 if test.expectedClientSecret != respSecret {
540 t.Errorf("Expected response secret to be '%s', got '%s'", test.expectedClientSecret, respSecret)
542 if test.expectedValid != success {
543 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
545 if test.expectedCode != w.Code {
546 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
548 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
549 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
551 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
552 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
557 func TestVerifyClient(t *testing.T) {
559 type verifyClientRequest struct {
564 expectedClientID uuid.ID
568 expectAuthenticateHeader bool
570 memstore := NewMemstore()
576 Secret: "super secret!",
577 OwnerID: uuid.NewID(),
578 Name: "My test client",
579 Logo: "https://secondbit.org/logo.png",
580 Website: "https://secondbit.org/",
581 Type: "confidential",
583 err := context.SaveClient(client)
585 t.Fatal("Could not save client:", err)
587 publicClient := Client{
590 OwnerID: uuid.NewID(),
591 Name: "A public client",
592 Logo: "https://secondbit.org/logo.png",
593 Website: "https://secondbit.org/",
596 err = context.SaveClient(publicClient)
598 t.Fatal("Could not save client:", err)
601 tests := []verifyClientRequest{
602 {"", "", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
603 {"", "", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
604 {"", "no clientID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
605 {"", "no clientID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
606 {"not an actual id", "invalid client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
607 {"not an actual id", "invalid client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
608 {id.String(), "unsaved client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
609 {id.String(), "unsaved client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
610 {client.ID.String(), "wrong secret", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
611 {client.ID.String(), "wrong secret", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
612 {"", "", "not an actual id", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
613 {"", "", id.String(), true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
614 {client.ID.String(), client.Secret, "", true, client.ID, true, http.StatusOK, "", false},
615 {client.ID.String(), client.Secret, "", false, client.ID, true, http.StatusOK, "", false},
616 {"", "", publicClient.ID.String(), true, publicClient.ID, true, http.StatusOK, "", false},
617 {"", "", publicClient.ID.String(), false, nil, false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
620 for pos, test := range tests {
621 t.Logf("Running test #%d, with request %+v", pos, test)
622 w := httptest.NewRecorder()
623 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
625 t.Fatal("Can't build request:", err)
627 if test.username != "" || test.pass != "" {
628 r.SetBasicAuth(test.username, test.pass)
630 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
631 params := url.Values{}
632 params.Set("client_id", test.clientID)
633 body := bytes.NewBufferString(params.Encode())
634 r.Body = ioutil.NopCloser(body)
635 respID, success := verifyClient(w, r, test.allowPublic, context)
636 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
637 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
639 if test.expectedValid != success {
640 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
642 if test.expectedCode != w.Code {
643 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
645 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
646 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
648 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
649 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
654 func TestCreateClientHandler(t *testing.T) {
656 memstore := NewMemstore()
661 w := httptest.NewRecorder()
662 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/clients", nil)
664 t.Fatal("Can't build request:", err)
666 r.Header.Set("Content-Type", "application/json")
667 CreateClientHandler(w, r, c)
668 if w.Code != http.StatusUnauthorized {
669 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
671 expected := `{"errors":[{"error":"access_denied"}]}`
672 result := strings.TrimSpace(w.Body.String())
673 if result != expected {
674 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
676 w = httptest.NewRecorder()
677 r.Header.Set("Authorization", "Not basic at all...")
678 CreateClientHandler(w, r, c)
679 if w.Code != http.StatusUnauthorized {
680 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
682 expected = `{"errors":[{"error":"access_denied"}]}`
683 result = strings.TrimSpace(w.Body.String())
684 if result != expected {
685 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
687 w = httptest.NewRecorder()
688 r.Header.Set("Authorization", "Basic TotallyNotBase64Encoded")
689 CreateClientHandler(w, r, c)
690 if w.Code != http.StatusUnauthorized {
691 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
693 expected = `{"errors":[{"error":"access_denied"}]}`
694 result = strings.TrimSpace(w.Body.String())
695 if result != expected {
696 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
698 w = httptest.NewRecorder()
699 r.Header.Set("Authorization", "Basic dGhpc2hhc25vY29sb24=")
700 CreateClientHandler(w, r, c)
701 if w.Code != http.StatusUnauthorized {
702 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
704 expected = `{"errors":[{"error":"access_denied"}]}`
705 result = strings.TrimSpace(w.Body.String())
706 if result != expected {
707 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
712 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
714 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
717 LockedUntil: time.Time{},
719 PassphraseResetCreated: time.Time{},
721 LastSeen: time.Time{},
725 Value: "test@example.com",
726 ProfileID: profile.ID,
728 LastUsed: time.Time{},
730 w = httptest.NewRecorder()
731 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
732 CreateClientHandler(w, r, c)
733 if w.Code != http.StatusUnauthorized {
734 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
736 expected = `{"errors":[{"error":"access_denied"}]}`
737 result = strings.TrimSpace(w.Body.String())
738 if result != expected {
739 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
741 err = c.SaveProfile(profile)
743 t.Error("Error saving profile:", err)
745 err = c.AddLogin(login)
747 t.Error("Error adding login:", err)
749 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
750 type testStruct struct {
755 tests := []testStruct{
756 {``, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/"}}}},
757 {`{}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
758 {`{"type":"notarealtype"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
759 {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrOverflow, Field: "/name"}}}},
760 {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrInsufficient, Field: "/name"}}}},
761 {`{"type":"public"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/name"}}}},
762 {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrOverflow, Field: "/name"}}}},
763 {`{"type":"public","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInsufficient, Field: "/name"}}}},
764 {`{"name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}}}},
765 {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}}}},
766 {`{"type":"public","name":"My Client"}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}},
767 {`{"type":"public","name":"My Client", "endpoints": ["https://test.secondbit.org/", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://test.secondbit.org/"}, {URI: "https://paddy.io"}}}},
768 {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/endpoints/0"}}}},
769 {`{"type":"public","name":"My Client", "endpoints": [":/not a url", "/relative/uri", "https://paddy.io"]}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}, Endpoints: []Endpoint{{URI: "https://paddy.io"}}, Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/endpoints/0"}, {Slug: requestErrInvalidValue, Field: "/endpoints/1"}}}},
770 {`{"type":"confidential","name":"Secret Client", "endpoints": ["https://secondbit.org"]}`, http.StatusCreated, response{Clients: []Client{{Name: "Secret Client", OwnerID: profile.ID, Type: "confidential"}}, Endpoints: []Endpoint{{URI: "https://secondbit.org"}}}},
772 for pos, test := range tests {
773 t.Logf("Test #%d: `%s`", pos, test.request)
774 w = httptest.NewRecorder()
775 body := bytes.NewBufferString(test.request)
776 r.Body = ioutil.NopCloser(body)
777 CreateClientHandler(w, r, c)
778 if w.Code != test.code {
779 t.Errorf("Expected response code to be %d, got %d", test.code, w.Code)
781 t.Logf("Response: %s", w.Body.String())
783 err = json.Unmarshal(w.Body.Bytes(), &res)
785 t.Error("Unexpected error unmarshalling response:", err)
787 if len(res.Clients) > 0 {
788 if res.Clients[0].Type == "confidential" && res.Clients[0].Secret == "" {
789 t.Log("Client:", res.Clients[0])
790 t.Error("Expected confidential client to have a secret, but does not.")
791 } else if res.Clients[0].Type == "public" && res.Clients[0].Secret != "" {
792 t.Log("Client:", res.Clients[0])
793 t.Error("Expected public client to not have a secret, but it does.")
796 fillInServerGenerated(test.resp, res)
797 success, field, expectation, result := compareResponses(test.resp, res)
799 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
804 func TestGetClientHandler(t *testing.T) {
806 memstore := NewMemstore()
813 Secret: "myawesomesecret",
814 OwnerID: uuid.NewID(),
816 Logo: "https://auth.secondbit.org/logo.png",
817 Website: "https://code.secondbit.org",
818 Type: clientTypeConfidential,
820 err := c.SaveClient(client)
822 t.Fatal("Can't store client in memstore:", err)
827 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
829 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
832 LockedUntil: time.Time{},
834 PassphraseResetCreated: time.Time{},
836 LastSeen: time.Time{},
840 Value: "test@example.com",
841 ProfileID: profile.ID,
843 LastUsed: time.Time{},
845 err = c.SaveProfile(profile)
847 t.Error("Error saving profile:", err)
849 err = c.AddLogin(login)
851 t.Error("Error adding login:", err)
853 router := mux.NewRouter()
854 RegisterClientHandlers(router, c)
855 w := httptest.NewRecorder()
856 u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
857 r, err := http.NewRequest("GET", u, nil)
859 t.Fatal("Can't build request:", err)
861 r.Header.Set("Content-Type", "application/json")
862 router.ServeHTTP(w, r)
863 if w.Code != http.StatusOK {
864 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
866 t.Logf("Response: %s", w.Body.String())
868 err = json.Unmarshal(w.Body.Bytes(), &res)
870 t.Error("Unexpected error unmarshalling response:", err)
872 if len(res.Clients) != 1 {
873 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
875 if res.Clients[0].Secret != "" {
876 t.Error("Expected secret not to be set, but was set to", res.Clients[0].Secret)
878 // fill in the secret, which was omitted in the response
879 res.Clients[0].Secret = client.Secret
880 success, field, expectation, result := compareClients(client, res.Clients[0])
882 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
885 // test for improperly formatted ID
886 u = "https://test.auth.secondbit.org/clients/notanID"
887 w = httptest.NewRecorder()
888 r, err = http.NewRequest("GET", u, nil)
890 t.Fatal("Can't build request:", err)
892 r.Header.Set("Content-Type", "application/json")
893 router.ServeHTTP(w, r)
894 if w.Code != http.StatusBadRequest {
895 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
897 t.Logf("Response: %s", w.Body.String())
899 err = json.Unmarshal(w.Body.Bytes(), &res)
901 t.Error("Unexpected error unmarshalling response:", err)
903 if len(res.Errors) != 1 {
904 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
906 e := requestError{Slug: requestErrInvalidFormat, Param: "id"}
907 success, field, expectation, result = compareErrors(e, res.Errors[0])
909 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
912 // test for a non-existent client
913 u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
914 w = httptest.NewRecorder()
915 r, err = http.NewRequest("GET", u, nil)
917 t.Fatal("Can't build request:", err)
919 r.Header.Set("Content-Type", "application/json")
920 router.ServeHTTP(w, r)
921 if w.Code != http.StatusNotFound {
922 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
924 t.Logf("Response: %s", w.Body.String())
926 err = json.Unmarshal(w.Body.Bytes(), &res)
928 t.Error("Unexpected error unmarshalling response:", err)
930 if len(res.Errors) != 1 {
931 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
933 e = requestError{Slug: requestErrNotFound, Param: "id"}
934 success, field, expectation, result = compareErrors(e, res.Errors[0])
936 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
940 func TestAuthenticatedGetClientHandler(t *testing.T) {
942 memstore := NewMemstore()
949 Secret: "myawesomesecret",
950 OwnerID: uuid.NewID(),
952 Logo: "https://auth.secondbit.org/logo.png",
953 Website: "https://code.secondbit.org",
954 Type: clientTypeConfidential,
956 err := c.SaveClient(client)
958 t.Fatal("Can't store client in memstore:", err)
963 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
965 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
968 LockedUntil: time.Time{},
970 PassphraseResetCreated: time.Time{},
972 LastSeen: time.Time{},
976 Value: "test@example.com",
977 ProfileID: profile.ID,
979 LastUsed: time.Time{},
981 err = c.SaveProfile(profile)
983 t.Error("Error saving profile:", err)
985 err = c.AddLogin(login)
987 t.Error("Error adding login:", err)
992 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
994 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
997 LockedUntil: time.Time{},
999 PassphraseResetCreated: time.Time{},
1000 Created: time.Now(),
1001 LastSeen: time.Time{},
1005 Value: "test2@example.com",
1006 ProfileID: profile2.ID,
1007 Created: time.Now(),
1008 LastUsed: time.Time{},
1010 err = c.SaveProfile(profile2)
1012 t.Error("Error saving profile:", err)
1014 err = c.AddLogin(login2)
1016 t.Error("Error adding login:", err)
1018 router := mux.NewRouter()
1019 RegisterClientHandlers(router, c)
1020 w := httptest.NewRecorder()
1021 u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
1022 r, err := http.NewRequest("GET", u, nil)
1024 t.Fatal("Can't build request:", err)
1026 r.Header.Set("Content-Type", "application/json")
1027 r.SetBasicAuth(login.Value, "mysecurepassphrase")
1028 router.ServeHTTP(w, r)
1029 if w.Code != http.StatusOK {
1030 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
1032 t.Logf("Response: %s", w.Body.String())
1034 err = json.Unmarshal(w.Body.Bytes(), &res)
1036 t.Error("Unexpected error unmarshalling response:", err)
1038 if len(res.Clients) != 1 {
1039 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
1041 success, field, expectation, result := compareClients(client, res.Clients[0])
1043 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
1046 // test for improperly formatted ID
1047 u = "https://test.auth.secondbit.org/clients/notanID"
1048 w = httptest.NewRecorder()
1049 r, err = http.NewRequest("GET", u, nil)
1051 t.Fatal("Can't build request:", err)
1053 r.Header.Set("Content-Type", "application/json")
1054 r.SetBasicAuth(login.Value, "mysecurepassphrase")
1055 router.ServeHTTP(w, r)
1056 if w.Code != http.StatusBadRequest {
1057 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
1059 t.Logf("Response: %s", w.Body.String())
1061 err = json.Unmarshal(w.Body.Bytes(), &res)
1063 t.Error("Unexpected error unmarshalling response:", err)
1065 if len(res.Errors) != 1 {
1066 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
1068 e := requestError{Slug: requestErrInvalidFormat, Param: "id"}
1069 success, field, expectation, result = compareErrors(e, res.Errors[0])
1071 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
1074 // test for a non-existent client
1075 u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
1076 w = httptest.NewRecorder()
1077 r, err = http.NewRequest("GET", u, nil)
1079 t.Fatal("Can't build request:", err)
1081 r.Header.Set("Content-Type", "application/json")
1082 r.SetBasicAuth(login.Value, "mysecurepassphrase")
1083 router.ServeHTTP(w, r)
1084 if w.Code != http.StatusNotFound {
1085 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
1087 t.Logf("Response: %s", w.Body.String())
1089 err = json.Unmarshal(w.Body.Bytes(), &res)
1091 t.Error("Unexpected error unmarshalling response:", err)
1093 if len(res.Errors) != 1 {
1094 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
1096 e = requestError{Slug: requestErrNotFound, Param: "id"}
1097 success, field, expectation, result = compareErrors(e, res.Errors[0])
1099 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
1102 // test for a wrong password
1103 u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
1104 w = httptest.NewRecorder()
1105 r, err = http.NewRequest("GET", u, nil)
1107 t.Fatal("Can't build request:", err)
1109 r.Header.Set("Content-Type", "application/json")
1110 r.SetBasicAuth(login.Value, "notmypassphrase")
1111 router.ServeHTTP(w, r)
1112 if w.Code != http.StatusUnauthorized {
1113 t.Errorf("Expected response code to be %d, got %d", http.StatusUnauthorized, w.Code)
1115 t.Logf("Response: %s", w.Body.String())
1117 err = json.Unmarshal(w.Body.Bytes(), &res)
1119 t.Error("Unexpected error unmarshalling response:", err)
1121 if len(res.Errors) != 1 {
1122 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
1124 e = requestError{Slug: requestErrAccessDenied}
1125 success, field, expectation, result = compareErrors(e, res.Errors[0])
1127 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
1130 // test for a wrong account
1131 u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
1132 w = httptest.NewRecorder()
1133 r, err = http.NewRequest("GET", u, nil)
1135 t.Fatal("Can't build request:", err)
1137 r.Header.Set("Content-Type", "application/json")
1138 r.SetBasicAuth(login2.Value, "mysecurepassphrase")
1139 router.ServeHTTP(w, r)
1140 if w.Code != http.StatusOK {
1141 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
1143 t.Logf("Response: %s", w.Body.String())
1145 err = json.Unmarshal(w.Body.Bytes(), &res)
1147 t.Error("Unexpected error unmarshalling response:", err)
1149 if len(res.Clients) != 1 {
1150 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
1152 if res.Clients[0].Secret != "" {
1153 t.Errorf("Expected client secret to be empty, got %s", res.Clients[0].Secret)
1155 // fill the client's secret for comparison
1156 res.Clients[0].Secret = client.Secret
1157 success, field, expectation, result = compareClients(client, res.Clients[0])
1159 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
1163 // BUG(paddy): We need to test the clientCredentialsValidate function.
1164 // BUG(paddy): We need to test the ListClientsHandler.