Implement postgres version of scopeStore.
Update the authd server to use postgres as its scopeStore, instead of memstore.
panic when starting the authd server if the CreateScopes call fails. This
should, ideally, ignore ErrScopeAlreadyExists errors, but does not as of this
commit.
Update the simple.gotmpl template to properly display scopes, after switching to
the Scope type instead of simply passing around the string the client supplied
broke the template and I never bothered fixing it.
Update the updateScopes method on the scopeStore (and the corresponding
UpdateScopes method on the Context type) to be updateScope/UpdateScope.
Operating on several scopes at a time like that is simply too challenging in
SQL and I can't justify the complexity with a use case.
Add a helper method to ScopeChange called Empty(), which returns true if the
ScopeChange is full of nil values.
Remove the ID from the ScopeChange type, because we're no longer accepting
multiple ScopeChange types in UpdateScope, so we can supply that information
outside the ScopeChange, which matches the rest of our update* methods.
Correct our tests in scope_test.go to correctly use the updateScope method
instead of the old updateScopes method. This generally just resulted in calling
updateScope multiple times, as opposed to just once.
Add a scope table initialization to the sql/postgres_init.sql script.
7 "github.com/gorilla/mux"
17 "code.secondbit.org/uuid.hg"
21 clientChangeSecret = 1 << iota
29 p, err := NewPostgres("dbname=testdb sslmode=disable")
34 clientStores = append(clientStores, &p)
38 var clientStores = []clientStore{NewMemstore()}
40 func compareClients(client1, client2 Client) (success bool, field string, val1, val2 interface{}) {
41 if !client1.ID.Equal(client2.ID) {
42 return false, "ID", client1.ID, client2.ID
44 if client1.Secret != client2.Secret {
45 return false, "secret", client1.Secret, client2.Secret
47 if !client1.OwnerID.Equal(client2.OwnerID) {
48 return false, "owner ID", client1.OwnerID, client2.OwnerID
50 if client1.Name != client2.Name {
51 return false, "name", client1.Name, client2.Name
53 if client1.Logo != client2.Logo {
54 return false, "logo", client1.Logo, client2.Logo
56 if client1.Website != client2.Website {
57 return false, "website", client1.Website, client2.Website
59 if client1.Type != client2.Type {
60 return false, "type", client1.Type, client2.Type
62 return true, "", nil, nil
65 func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) {
66 if !endpoint1.ID.Equal(endpoint2.ID) {
67 return false, "ID", endpoint1.ID, endpoint2.ID
69 if !endpoint1.ClientID.Equal(endpoint2.ClientID) {
70 return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID
72 if !endpoint1.Added.Equal(endpoint2.Added) {
73 return false, "Added", endpoint1.Added, endpoint2.Added
75 if endpoint1.URI != endpoint2.URI {
76 return false, "URI", endpoint1.URI, endpoint2.URI
78 return true, "", nil, nil
81 func TestClientStoreSuccess(t *testing.T) {
86 OwnerID: uuid.NewID(),
91 for _, store := range clientStores {
92 context := Context{clients: store}
93 err := context.SaveClient(client)
95 t.Fatalf("Error saving client to %T: %s", store, err)
97 err = context.SaveClient(client)
98 if err != ErrClientAlreadyExists {
99 t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
101 retrieved, err := context.GetClient(client.ID)
103 t.Fatalf("Error retrieving client from %T: %s", store, err)
105 success, field, expectation, result := compareClients(client, retrieved)
107 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
109 clients, err := context.ListClientsByOwner(client.OwnerID, 25, 0)
111 t.Fatalf("Error retrieving clients by owner from %T: %s", store, err)
113 if len(clients) != 1 {
114 t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients)
116 success, field, expectation, result = compareClients(client, clients[0])
118 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
121 err = context.UpdateClient(client.ID, ClientChange{Deleted: &deleted})
123 t.Fatalf("Error deleting client from %T: %s", store, err)
125 retrieved, err = context.GetClient(client.ID)
126 if err != ErrClientNotFound {
127 t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
129 clients, err = context.ListClientsByOwner(client.OwnerID, 25, 0)
131 t.Fatalf("Error listing clients by owner from %T: %s", store, err)
133 if len(clients) != 0 {
134 t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients)
139 func TestEndpointStoreSuccess(t *testing.T) {
144 OwnerID: uuid.NewID(),
149 endpoint1 := Endpoint{
152 Added: time.Now().Round(time.Millisecond),
153 URI: "https://www.example.com/",
155 endpoint2 := Endpoint{
158 Added: time.Now().Round(time.Millisecond),
159 URI: "https://www.example.com/my/full/path",
161 for _, store := range clientStores {
162 context := Context{clients: store}
163 err := context.SaveClient(client)
165 t.Fatalf("Error saving client to %T: %s", store, err)
167 err = context.AddEndpoints([]Endpoint{endpoint1})
169 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
171 endpoints, err := context.ListEndpoints(client.ID, 10, 0)
173 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
175 if len(endpoints) != 1 {
176 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
178 success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0])
180 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
182 err = context.AddEndpoints([]Endpoint{endpoint2})
184 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
186 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
188 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
190 if len(endpoints) != 2 {
191 t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store)
193 sortedEnd := sortedEndpoints(endpoints)
195 endpoints = []Endpoint(sortedEnd)
196 success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0])
198 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
200 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1])
202 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
204 err = context.RemoveEndpoint(client.ID, endpoint1.ID)
206 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
208 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
210 t.Fatalf("Error listing endpoints in %T: %s", store, err)
212 if len(endpoints) != 1 {
213 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
215 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0])
217 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
219 err = context.RemoveEndpoint(client.ID, endpoint2.ID)
221 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
223 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
225 t.Fatalf("Error listing endpoints in %T: %s", store, err)
227 if len(endpoints) != 0 {
228 t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store)
233 func TestClientUpdates(t *testing.T) {
239 OwnerID: uuid.NewID(),
244 for i := 0; i < variations; i++ {
245 var secret, name, logo, website string
246 change := ClientChange{}
247 client.ID = uuid.NewID()
248 expectation := client
250 if i&clientChangeSecret != 0 {
251 secret = fmt.Sprintf("secret-%d", i)
252 change.Secret = &secret
253 expectation.Secret = secret
255 if i&clientChangeOwnerID != 0 {
256 change.OwnerID = uuid.NewID()
257 expectation.OwnerID = change.OwnerID
259 if i&clientChangeName != 0 {
260 name = fmt.Sprintf("name-%d", i)
262 expectation.Name = name
264 if i&clientChangeLogo != 0 {
265 logo = fmt.Sprintf("logo-%d", i)
267 expectation.Logo = logo
269 if i&clientChangeWebsite != 0 {
270 website = fmt.Sprintf("website-%d", i)
271 change.Website = &website
272 expectation.Website = website
274 result.ApplyChange(change)
275 match, field, expected, got := compareClients(expectation, result)
277 t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
279 for _, store := range clientStores {
280 context := Context{clients: store}
281 err := context.SaveClient(client)
283 t.Fatalf("Error saving client in %T: %s", store, err)
285 err = context.UpdateClient(client.ID, change)
287 t.Fatalf("Error updating client in %T: %s", store, err)
289 retrieved, err := context.GetClient(client.ID)
291 t.Fatalf("Error getting client from %T: %s", store, err)
293 match, field, expected, got = compareClients(expectation, retrieved)
295 t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
298 err = context.UpdateClient(client.ID, ClientChange{Deleted: &deleted})
300 t.Fatalf("Error deleting client from %T: %s", store, err)
306 func TestClientEndpointChecks(t *testing.T) {
311 OwnerID: uuid.NewID(),
316 endpoint1 := Endpoint{
319 Added: time.Now().Round(time.Millisecond),
320 URI: "https://www.example.com/first",
322 endpoint2 := Endpoint{
325 Added: time.Now().Round(time.Millisecond),
326 URI: "https://www.example.com/my/full/path",
328 candidates := map[string]bool{
329 "https://www.example.com/": false,
330 "https://www.example.com/first": true,
331 "https://www.example.com/first/extra/path": false,
332 "https://www.example.com/my": false,
333 "https://www.example.com/my/full/path": true,
335 for _, store := range clientStores {
336 context := Context{clients: store}
337 err := context.SaveClient(client)
339 t.Fatalf("Error saving client in %T: %s", store, err)
341 err = context.AddEndpoints([]Endpoint{endpoint1})
343 t.Fatalf("Error saving endpoint in %T: %s", store, err)
345 err = context.AddEndpoints([]Endpoint{endpoint2})
347 t.Fatalf("Error saving endpoint in %T: %s", store, err)
349 for candidate, expectation := range candidates {
350 result, err := context.CheckEndpoint(client.ID, candidate)
352 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
354 if result != expectation {
361 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
367 func TestClientEndpointChecksStrict(t *testing.T) {
372 OwnerID: uuid.NewID(),
377 endpoint1 := Endpoint{
380 Added: time.Now().Round(time.Millisecond),
381 URI: "https://www.example.com/first",
383 endpoint2 := Endpoint{
386 Added: time.Now().Round(time.Millisecond),
387 URI: "https://www.example.com/my/full/path",
389 candidates := map[string]bool{
390 "https://www.example.com/": false,
391 "https://www.example.com/first": true,
392 "https://www.example.com/first/extra/path": false,
393 "https://www.example.com/my": false,
394 "https://www.example.com/my/full/path": true,
396 for _, store := range clientStores {
397 context := Context{clients: store}
398 err := context.SaveClient(client)
400 t.Fatalf("Error saving client in %T: %s", store, err)
402 err = context.AddEndpoints([]Endpoint{endpoint1})
404 t.Fatalf("Error saving endpoint in %T: %s", store, err)
406 err = context.AddEndpoints([]Endpoint{endpoint2})
408 t.Fatalf("Error saving endpoint in %T: %s", store, err)
410 for candidate, expectation := range candidates {
411 result, err := context.CheckEndpoint(client.ID, candidate)
413 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
415 if result != expectation {
422 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
428 func TestClientChangeValidation(t *testing.T) {
430 change := ClientChange{}
431 if err := change.Validate(); err[0] != ErrEmptyChange {
432 t.Errorf("Expected %s to give an error of %s, gave %s", "empty change", ErrEmptyChange, err)
434 names := map[string][]error{
435 "a": []error{ErrClientNameTooShort},
438 "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": []error{ErrClientNameTooLong},
440 for name, expectation := range names {
441 change = ClientChange{Name: &name}
442 errs := change.Validate()
443 if len(errs) != len(expectation) {
444 t.Errorf("Expected %s to give %d errors, gave %d", name, len(expectation), len(errs))
447 for pos, err := range errs {
448 if err != expectation[pos] {
449 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", name, expectation[pos], pos, err)
454 for i := 0; i < 1025; i++ {
455 longPath = fmt.Sprintf("%s%d", longPath, i)
457 logos := map[string][]error{
458 "https://www.example.com/" + longPath: []error{ErrClientLogoTooLong},
459 "https://www.example.com/ab": []error{},
460 "www.example.com/ab": []error{ErrClientLogoNotURL},
461 "test": []error{ErrClientLogoNotURL},
464 for logo, expectation := range logos {
465 change = ClientChange{Logo: &logo}
466 errs := change.Validate()
467 if len(errs) != len(expectation) {
468 t.Errorf("Expected %s to give %d errors, gave %d", logo, len(expectation), len(errs))
470 for pos, err := range errs {
471 if err != expectation[pos] {
472 t.Errorf("Expected %s to give an error of %s in positiong %d, gave %s", logo, expectation[pos], pos, err)
476 websites := map[string][]error{
477 "https://www.example.com/" + longPath: []error{ErrClientWebsiteTooLong},
478 "https://www.example.com/ab": []error{},
479 "www.example.com/ab": []error{ErrClientWebsiteNotURL},
480 "test": []error{ErrClientWebsiteNotURL},
483 for website, expectation := range websites {
484 change = ClientChange{Website: &website}
485 errs := change.Validate()
486 if len(errs) != len(expectation) {
487 t.Errorf("Expected %s to give %d errors, gave %d", website, len(expectation), len(errs))
489 for pos, err := range errs {
490 if err != expectation[pos] {
491 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", website, expectation[pos], pos, err)
497 func TestGetClientAuth(t *testing.T) {
499 type clientAuthRequest struct {
504 expectedClientID uuid.ID
505 expectedClientSecret string
509 expectAuthenticateHeader bool
512 tests := []clientAuthRequest{
513 {"", "", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
514 {"", "", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
515 {"", "no clientID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
516 {"", "no clientID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
517 {"not an actual id", "invalid client ID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
518 {"not an actual id", "invalid client ID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
519 {"", "", "not an actual id", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
520 {id.String(), "secret", "", true, id, "secret", true, http.StatusOK, "", false},
521 {id.String(), "secret", "", false, id, "secret", true, http.StatusOK, "", false},
522 {"", "", id.String(), true, id, "", true, http.StatusOK, "", false},
523 {"", "", id.String(), false, nil, "", false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
525 for pos, test := range tests {
526 t.Logf("Running test #%d, with request %+v", pos, test)
527 w := httptest.NewRecorder()
528 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
530 t.Fatal("Can't build request:", err)
532 if test.username != "" || test.pass != "" {
533 r.SetBasicAuth(test.username, test.pass)
535 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
536 params := url.Values{}
537 params.Set("client_id", test.clientID)
538 body := bytes.NewBufferString(params.Encode())
539 r.Body = ioutil.NopCloser(body)
540 respID, respSecret, success := getClientAuth(w, r, test.allowPublic)
541 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
542 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
544 if test.expectedClientSecret != respSecret {
545 t.Errorf("Expected response secret to be '%s', got '%s'", test.expectedClientSecret, respSecret)
547 if test.expectedValid != success {
548 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
550 if test.expectedCode != w.Code {
551 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
553 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
554 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
556 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
557 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
562 func TestVerifyClient(t *testing.T) {
564 type verifyClientRequest struct {
569 expectedClientID uuid.ID
573 expectAuthenticateHeader bool
575 memstore := NewMemstore()
581 Secret: "super secret!",
582 OwnerID: uuid.NewID(),
583 Name: "My test client",
584 Logo: "https://secondbit.org/logo.png",
585 Website: "https://secondbit.org/",
586 Type: "confidential",
588 err := context.SaveClient(client)
590 t.Fatal("Could not save client:", err)
592 publicClient := Client{
595 OwnerID: uuid.NewID(),
596 Name: "A public client",
597 Logo: "https://secondbit.org/logo.png",
598 Website: "https://secondbit.org/",
601 err = context.SaveClient(publicClient)
603 t.Fatal("Could not save client:", err)
606 tests := []verifyClientRequest{
607 {"", "", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
608 {"", "", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
609 {"", "no clientID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
610 {"", "no clientID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
611 {"not an actual id", "invalid client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
612 {"not an actual id", "invalid client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
613 {id.String(), "unsaved client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
614 {id.String(), "unsaved client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
615 {client.ID.String(), "wrong secret", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
616 {client.ID.String(), "wrong secret", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
617 {"", "", "not an actual id", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
618 {"", "", id.String(), true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
619 {client.ID.String(), client.Secret, "", true, client.ID, true, http.StatusOK, "", false},
620 {client.ID.String(), client.Secret, "", false, client.ID, true, http.StatusOK, "", false},
621 {"", "", publicClient.ID.String(), true, publicClient.ID, true, http.StatusOK, "", false},
622 {"", "", publicClient.ID.String(), false, nil, false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
625 for pos, test := range tests {
626 t.Logf("Running test #%d, with request %+v", pos, test)
627 w := httptest.NewRecorder()
628 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
630 t.Fatal("Can't build request:", err)
632 if test.username != "" || test.pass != "" {
633 r.SetBasicAuth(test.username, test.pass)
635 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
636 params := url.Values{}
637 params.Set("client_id", test.clientID)
638 body := bytes.NewBufferString(params.Encode())
639 r.Body = ioutil.NopCloser(body)
640 respID, success := verifyClient(w, r, test.allowPublic, context)
641 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
642 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
644 if test.expectedValid != success {
645 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
647 if test.expectedCode != w.Code {
648 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
650 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
651 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
653 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
654 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
659 func TestCreateClientHandler(t *testing.T) {
661 memstore := NewMemstore()
666 w := httptest.NewRecorder()
667 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/clients", nil)
669 t.Fatal("Can't build request:", err)
671 r.Header.Set("Content-Type", "application/json")
672 CreateClientHandler(w, r, c)
673 if w.Code != http.StatusUnauthorized {
674 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
676 expected := `{"errors":[{"error":"access_denied"}]}`
677 result := strings.TrimSpace(w.Body.String())
678 if result != expected {
679 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
681 w = httptest.NewRecorder()
682 r.Header.Set("Authorization", "Not basic at all...")
683 CreateClientHandler(w, r, c)
684 if w.Code != http.StatusUnauthorized {
685 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
687 expected = `{"errors":[{"error":"access_denied"}]}`
688 result = strings.TrimSpace(w.Body.String())
689 if result != expected {
690 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
692 w = httptest.NewRecorder()
693 r.Header.Set("Authorization", "Basic TotallyNotBase64Encoded")
694 CreateClientHandler(w, r, c)
695 if w.Code != http.StatusUnauthorized {
696 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
698 expected = `{"errors":[{"error":"access_denied"}]}`
699 result = strings.TrimSpace(w.Body.String())
700 if result != expected {
701 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
703 w = httptest.NewRecorder()
704 r.Header.Set("Authorization", "Basic dGhpc2hhc25vY29sb24=")
705 CreateClientHandler(w, r, c)
706 if w.Code != http.StatusUnauthorized {
707 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
709 expected = `{"errors":[{"error":"access_denied"}]}`
710 result = strings.TrimSpace(w.Body.String())
711 if result != expected {
712 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
717 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
719 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
722 LockedUntil: time.Time{},
724 PassphraseResetCreated: time.Time{},
725 Created: time.Now().Round(time.Millisecond),
726 LastSeen: time.Time{},
730 Value: "test@example.com",
731 ProfileID: profile.ID,
732 Created: time.Now().Round(time.Millisecond),
733 LastUsed: time.Time{},
735 w = httptest.NewRecorder()
736 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
737 CreateClientHandler(w, r, c)
738 if w.Code != http.StatusUnauthorized {
739 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
741 expected = `{"errors":[{"error":"access_denied"}]}`
742 result = strings.TrimSpace(w.Body.String())
743 if result != expected {
744 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
746 err = c.SaveProfile(profile)
748 t.Error("Error saving profile:", err)
750 err = c.AddLogin(login)
752 t.Error("Error adding login:", err)
754 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
755 type testStruct struct {
760 tests := []testStruct{
761 {``, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/"}}}},
762 {`{}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
763 {`{"type":"notarealtype"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
764 {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrOverflow, Field: "/name"}}}},
765 {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrInsufficient, Field: "/name"}}}},
766 {`{"type":"public"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/name"}}}},
767 {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrOverflow, Field: "/name"}}}},
768 {`{"type":"public","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInsufficient, Field: "/name"}}}},
769 {`{"name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}}}},
770 {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}}}},
771 {`{"type":"public","name":"My Client"}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}},
772 {`{"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"}}}},
773 {`{"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"}}}},
774 {`{"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"}}}},
775 {`{"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"}}}},
777 for pos, test := range tests {
778 t.Logf("Test #%d: `%s`", pos, test.request)
779 w = httptest.NewRecorder()
780 body := bytes.NewBufferString(test.request)
781 r.Body = ioutil.NopCloser(body)
782 CreateClientHandler(w, r, c)
783 if w.Code != test.code {
784 t.Errorf("Expected response code to be %d, got %d", test.code, w.Code)
786 t.Logf("Response: %s", w.Body.String())
788 err = json.Unmarshal(w.Body.Bytes(), &res)
790 t.Error("Unexpected error unmarshalling response:", err)
792 if len(res.Clients) > 0 {
793 if res.Clients[0].Type == "confidential" && res.Clients[0].Secret == "" {
794 t.Log("Client:", res.Clients[0])
795 t.Error("Expected confidential client to have a secret, but does not.")
796 } else if res.Clients[0].Type == "public" && res.Clients[0].Secret != "" {
797 t.Log("Client:", res.Clients[0])
798 t.Error("Expected public client to not have a secret, but it does.")
801 fillInServerGenerated(test.resp, res)
802 success, field, expectation, result := compareResponses(test.resp, res)
804 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
809 func TestGetClientHandler(t *testing.T) {
811 memstore := NewMemstore()
818 Secret: "myawesomesecret",
819 OwnerID: uuid.NewID(),
821 Logo: "https://auth.secondbit.org/logo.png",
822 Website: "https://code.secondbit.org",
823 Type: clientTypeConfidential,
825 err := c.SaveClient(client)
827 t.Fatal("Can't store client in memstore:", err)
832 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
834 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
837 LockedUntil: time.Time{},
839 PassphraseResetCreated: time.Time{},
840 Created: time.Now().Round(time.Millisecond),
841 LastSeen: time.Time{},
845 Value: "test@example.com",
846 ProfileID: profile.ID,
847 Created: time.Now().Round(time.Millisecond),
848 LastUsed: time.Time{},
850 err = c.SaveProfile(profile)
852 t.Error("Error saving profile:", err)
854 err = c.AddLogin(login)
856 t.Error("Error adding login:", err)
858 router := mux.NewRouter()
859 RegisterClientHandlers(router, c)
860 w := httptest.NewRecorder()
861 u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
862 r, err := http.NewRequest("GET", u, nil)
864 t.Fatal("Can't build request:", err)
866 r.Header.Set("Content-Type", "application/json")
867 router.ServeHTTP(w, r)
868 if w.Code != http.StatusOK {
869 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
871 t.Logf("Response: %s", w.Body.String())
873 err = json.Unmarshal(w.Body.Bytes(), &res)
875 t.Error("Unexpected error unmarshalling response:", err)
877 if len(res.Clients) != 1 {
878 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
880 if res.Clients[0].Secret != "" {
881 t.Error("Expected secret not to be set, but was set to", res.Clients[0].Secret)
883 // fill in the secret, which was omitted in the response
884 res.Clients[0].Secret = client.Secret
885 success, field, expectation, result := compareClients(client, res.Clients[0])
887 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
890 // test for improperly formatted ID
891 u = "https://test.auth.secondbit.org/clients/notanID"
892 w = httptest.NewRecorder()
893 r, err = http.NewRequest("GET", u, nil)
895 t.Fatal("Can't build request:", err)
897 r.Header.Set("Content-Type", "application/json")
898 router.ServeHTTP(w, r)
899 if w.Code != http.StatusBadRequest {
900 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
902 t.Logf("Response: %s", w.Body.String())
904 err = json.Unmarshal(w.Body.Bytes(), &res)
906 t.Error("Unexpected error unmarshalling response:", err)
908 if len(res.Errors) != 1 {
909 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
911 e := requestError{Slug: requestErrInvalidFormat, Param: "id"}
912 success, field, expectation, result = compareErrors(e, res.Errors[0])
914 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
917 // test for a non-existent client
918 u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
919 w = httptest.NewRecorder()
920 r, err = http.NewRequest("GET", u, nil)
922 t.Fatal("Can't build request:", err)
924 r.Header.Set("Content-Type", "application/json")
925 router.ServeHTTP(w, r)
926 if w.Code != http.StatusNotFound {
927 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
929 t.Logf("Response: %s", w.Body.String())
931 err = json.Unmarshal(w.Body.Bytes(), &res)
933 t.Error("Unexpected error unmarshalling response:", err)
935 if len(res.Errors) != 1 {
936 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
938 e = requestError{Slug: requestErrNotFound, Param: "id"}
939 success, field, expectation, result = compareErrors(e, res.Errors[0])
941 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
945 func TestAuthenticatedGetClientHandler(t *testing.T) {
947 memstore := NewMemstore()
954 Secret: "myawesomesecret",
955 OwnerID: uuid.NewID(),
957 Logo: "https://auth.secondbit.org/logo.png",
958 Website: "https://code.secondbit.org",
959 Type: clientTypeConfidential,
961 err := c.SaveClient(client)
963 t.Fatal("Can't store client in memstore:", err)
968 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
970 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
973 LockedUntil: time.Time{},
975 PassphraseResetCreated: time.Time{},
976 Created: time.Now().Round(time.Millisecond),
977 LastSeen: time.Time{},
981 Value: "test@example.com",
982 ProfileID: profile.ID,
983 Created: time.Now().Round(time.Millisecond),
984 LastUsed: time.Time{},
986 err = c.SaveProfile(profile)
988 t.Error("Error saving profile:", err)
990 err = c.AddLogin(login)
992 t.Error("Error adding login:", err)
997 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
999 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
1000 PassphraseScheme: 1,
1002 LockedUntil: time.Time{},
1003 PassphraseReset: "",
1004 PassphraseResetCreated: time.Time{},
1005 Created: time.Now().Round(time.Millisecond),
1006 LastSeen: time.Time{},
1010 Value: "test2@example.com",
1011 ProfileID: profile2.ID,
1012 Created: time.Now().Round(time.Millisecond),
1013 LastUsed: time.Time{},
1015 err = c.SaveProfile(profile2)
1017 t.Error("Error saving profile:", err)
1019 err = c.AddLogin(login2)
1021 t.Error("Error adding login:", err)
1023 router := mux.NewRouter()
1024 RegisterClientHandlers(router, c)
1025 w := httptest.NewRecorder()
1026 u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
1027 r, err := http.NewRequest("GET", u, nil)
1029 t.Fatal("Can't build request:", err)
1031 r.Header.Set("Content-Type", "application/json")
1032 r.SetBasicAuth(login.Value, "mysecurepassphrase")
1033 router.ServeHTTP(w, r)
1034 if w.Code != http.StatusOK {
1035 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
1037 t.Logf("Response: %s", w.Body.String())
1039 err = json.Unmarshal(w.Body.Bytes(), &res)
1041 t.Error("Unexpected error unmarshalling response:", err)
1043 if len(res.Clients) != 1 {
1044 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
1046 success, field, expectation, result := compareClients(client, res.Clients[0])
1048 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
1051 // test for improperly formatted ID
1052 u = "https://test.auth.secondbit.org/clients/notanID"
1053 w = httptest.NewRecorder()
1054 r, err = http.NewRequest("GET", u, nil)
1056 t.Fatal("Can't build request:", err)
1058 r.Header.Set("Content-Type", "application/json")
1059 r.SetBasicAuth(login.Value, "mysecurepassphrase")
1060 router.ServeHTTP(w, r)
1061 if w.Code != http.StatusBadRequest {
1062 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
1064 t.Logf("Response: %s", w.Body.String())
1066 err = json.Unmarshal(w.Body.Bytes(), &res)
1068 t.Error("Unexpected error unmarshalling response:", err)
1070 if len(res.Errors) != 1 {
1071 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
1073 e := requestError{Slug: requestErrInvalidFormat, Param: "id"}
1074 success, field, expectation, result = compareErrors(e, res.Errors[0])
1076 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
1079 // test for a non-existent client
1080 u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
1081 w = httptest.NewRecorder()
1082 r, err = http.NewRequest("GET", u, nil)
1084 t.Fatal("Can't build request:", err)
1086 r.Header.Set("Content-Type", "application/json")
1087 r.SetBasicAuth(login.Value, "mysecurepassphrase")
1088 router.ServeHTTP(w, r)
1089 if w.Code != http.StatusNotFound {
1090 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
1092 t.Logf("Response: %s", w.Body.String())
1094 err = json.Unmarshal(w.Body.Bytes(), &res)
1096 t.Error("Unexpected error unmarshalling response:", err)
1098 if len(res.Errors) != 1 {
1099 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
1101 e = requestError{Slug: requestErrNotFound, Param: "id"}
1102 success, field, expectation, result = compareErrors(e, res.Errors[0])
1104 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
1107 // test for a wrong password
1108 u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
1109 w = httptest.NewRecorder()
1110 r, err = http.NewRequest("GET", u, nil)
1112 t.Fatal("Can't build request:", err)
1114 r.Header.Set("Content-Type", "application/json")
1115 r.SetBasicAuth(login.Value, "notmypassphrase")
1116 router.ServeHTTP(w, r)
1117 if w.Code != http.StatusUnauthorized {
1118 t.Errorf("Expected response code to be %d, got %d", http.StatusUnauthorized, w.Code)
1120 t.Logf("Response: %s", w.Body.String())
1122 err = json.Unmarshal(w.Body.Bytes(), &res)
1124 t.Error("Unexpected error unmarshalling response:", err)
1126 if len(res.Errors) != 1 {
1127 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
1129 e = requestError{Slug: requestErrAccessDenied}
1130 success, field, expectation, result = compareErrors(e, res.Errors[0])
1132 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
1135 // test for a wrong account
1136 u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
1137 w = httptest.NewRecorder()
1138 r, err = http.NewRequest("GET", u, nil)
1140 t.Fatal("Can't build request:", err)
1142 r.Header.Set("Content-Type", "application/json")
1143 r.SetBasicAuth(login2.Value, "mysecurepassphrase")
1144 router.ServeHTTP(w, r)
1145 if w.Code != http.StatusOK {
1146 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
1148 t.Logf("Response: %s", w.Body.String())
1150 err = json.Unmarshal(w.Body.Bytes(), &res)
1152 t.Error("Unexpected error unmarshalling response:", err)
1154 if len(res.Clients) != 1 {
1155 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
1157 if res.Clients[0].Secret != "" {
1158 t.Errorf("Expected client secret to be empty, got %s", res.Clients[0].Secret)
1160 // fill the client's secret for comparison
1161 res.Clients[0].Secret = client.Secret
1162 success, field, expectation, result = compareClients(client, res.Clients[0])
1164 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
1168 // BUG(paddy): We need to test the clientCredentialsValidate function.
1169 // BUG(paddy): We need to test the ListClientsHandler.