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.
16 "code.secondbit.org/uuid.hg"
20 clientChangeSecret = 1 << iota
27 var clientStores = []clientStore{NewMemstore()}
29 func compareClients(client1, client2 Client) (success bool, field string, val1, val2 interface{}) {
30 if !client1.ID.Equal(client2.ID) {
31 return false, "ID", client1.ID, client2.ID
33 if client1.Secret != client2.Secret {
34 return false, "secret", client1.Secret, client2.Secret
36 if !client1.OwnerID.Equal(client2.OwnerID) {
37 return false, "owner ID", client1.OwnerID, client2.OwnerID
39 if client1.Name != client2.Name {
40 return false, "name", client1.Name, client2.Name
42 if client1.Logo != client2.Logo {
43 return false, "logo", client1.Logo, client2.Logo
45 if client1.Website != client2.Website {
46 return false, "website", client1.Website, client2.Website
48 if client1.Type != client2.Type {
49 return false, "type", client1.Type, client2.Type
51 return true, "", nil, nil
54 func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) {
55 if !endpoint1.ID.Equal(endpoint2.ID) {
56 return false, "ID", endpoint1.ID, endpoint2.ID
58 if !endpoint1.ClientID.Equal(endpoint2.ClientID) {
59 return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID
61 if !endpoint1.Added.Equal(endpoint2.Added) {
62 return false, "Added", endpoint1.Added, endpoint2.Added
64 if endpoint1.URI != endpoint2.URI {
65 return false, "URI", endpoint1.URI, endpoint2.URI
67 return true, "", nil, nil
70 func TestClientStoreSuccess(t *testing.T) {
75 OwnerID: uuid.NewID(),
80 for _, store := range clientStores {
81 context := Context{clients: store}
82 err := context.SaveClient(client)
84 t.Fatalf("Error saving client to %T: %s", store, err)
86 err = context.SaveClient(client)
87 if err != ErrClientAlreadyExists {
88 t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
90 retrieved, err := context.GetClient(client.ID)
92 t.Fatalf("Error retrieving client from %T: %s", store, err)
94 success, field, expectation, result := compareClients(client, retrieved)
96 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
98 clients, err := context.ListClientsByOwner(client.OwnerID, 25, 0)
100 t.Fatalf("Error retrieving clients by owner from %T: %s", store, err)
102 if len(clients) != 1 {
103 t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients)
105 success, field, expectation, result = compareClients(client, clients[0])
107 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
109 err = context.DeleteClient(client.ID)
111 t.Fatalf("Error deleting client from %T: %s", store, err)
113 err = context.DeleteClient(client.ID)
114 if err != ErrClientNotFound {
115 t.Fatalf("Expected ErrClientNotFound, got %s from %T", err, store)
117 retrieved, err = context.GetClient(client.ID)
118 if err != ErrClientNotFound {
119 t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
121 clients, err = context.ListClientsByOwner(client.OwnerID, 25, 0)
123 t.Fatalf("Error listing clients by owner from %T: %s", store, err)
125 if len(clients) != 0 {
126 t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients)
131 func TestEndpointStoreSuccess(t *testing.T) {
136 OwnerID: uuid.NewID(),
141 endpoint1 := Endpoint{
145 URI: "https://www.example.com/",
147 endpoint2 := Endpoint{
151 URI: "https://www.example.com/my/full/path",
153 for _, store := range clientStores {
154 context := Context{clients: store}
155 err := context.SaveClient(client)
157 t.Fatalf("Error saving client to %T: %s", store, err)
159 err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
161 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
163 endpoints, err := context.ListEndpoints(client.ID, 10, 0)
165 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
167 if len(endpoints) != 1 {
168 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
170 success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0])
172 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
174 err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
176 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
178 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
180 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
182 if len(endpoints) != 2 {
183 t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store)
185 sortedEnd := sortedEndpoints(endpoints)
187 endpoints = []Endpoint(sortedEnd)
188 success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0])
190 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
192 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1])
194 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
196 err = context.RemoveEndpoint(client.ID, endpoint1.ID)
198 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
200 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
202 t.Fatalf("Error listing endpoints in %T: %s", store, err)
204 if len(endpoints) != 1 {
205 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
207 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0])
209 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
211 err = context.RemoveEndpoint(client.ID, endpoint2.ID)
213 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
215 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
217 t.Fatalf("Error listing endpoints in %T: %s", store, err)
219 if len(endpoints) != 0 {
220 t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store)
225 func TestClientUpdates(t *testing.T) {
231 OwnerID: uuid.NewID(),
236 for i := 0; i < variations; i++ {
237 var secret, name, logo, website string
238 change := ClientChange{}
239 expectation := client
241 if i&clientChangeSecret != 0 {
242 secret = fmt.Sprintf("secret-%d", i)
243 change.Secret = &secret
244 expectation.Secret = secret
246 if i&clientChangeOwnerID != 0 {
247 change.OwnerID = uuid.NewID()
248 expectation.OwnerID = change.OwnerID
250 if i&clientChangeName != 0 {
251 name = fmt.Sprintf("name-%d", i)
253 expectation.Name = name
255 if i&clientChangeLogo != 0 {
256 logo = fmt.Sprintf("logo-%d", i)
258 expectation.Logo = logo
260 if i&clientChangeWebsite != 0 {
261 website = fmt.Sprintf("website-%d", i)
262 change.Website = &website
263 expectation.Website = website
265 result.ApplyChange(change)
266 match, field, expected, got := compareClients(expectation, result)
268 t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
270 for _, store := range clientStores {
271 context := Context{clients: store}
272 err := context.SaveClient(client)
274 t.Fatalf("Error saving client in %T: %s", store, err)
276 err = context.UpdateClient(client.ID, change)
278 t.Fatalf("Error updating client in %T: %s", store, err)
280 retrieved, err := context.GetClient(client.ID)
282 t.Fatalf("Error getting client from %T: %s", store, err)
284 match, field, expected, got = compareClients(expectation, retrieved)
286 t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
288 err = context.DeleteClient(client.ID)
290 t.Fatalf("Error deleting client from %T: %s", store, err)
292 err = context.UpdateClient(client.ID, change)
293 if err != ErrClientNotFound {
294 t.Fatalf("Expected ErrClientNotFound, got %v from %T", err, store)
300 func TestClientEndpointChecks(t *testing.T) {
305 OwnerID: uuid.NewID(),
310 endpoint1 := Endpoint{
314 URI: "https://www.example.com/first",
316 endpoint2 := Endpoint{
320 URI: "https://www.example.com/my/full/path",
322 candidates := map[string]bool{
323 "https://www.example.com/": false,
324 "https://www.example.com/first": true,
325 "https://www.example.com/first/extra/path": false,
326 "https://www.example.com/my": false,
327 "https://www.example.com/my/full/path": true,
329 for _, store := range clientStores {
330 context := Context{clients: store}
331 err := context.SaveClient(client)
333 t.Fatalf("Error saving client in %T: %s", store, err)
335 err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
337 t.Fatalf("Error saving endpoint in %T: %s", store, err)
339 err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
341 t.Fatalf("Error saving endpoint in %T: %s", store, err)
343 for candidate, expectation := range candidates {
344 result, err := context.CheckEndpoint(client.ID, candidate)
346 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
348 if result != expectation {
355 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
361 func TestClientEndpointChecksStrict(t *testing.T) {
366 OwnerID: uuid.NewID(),
371 endpoint1 := Endpoint{
375 URI: "https://www.example.com/first",
377 endpoint2 := Endpoint{
381 URI: "https://www.example.com/my/full/path",
383 candidates := map[string]bool{
384 "https://www.example.com/": false,
385 "https://www.example.com/first": true,
386 "https://www.example.com/first/extra/path": false,
387 "https://www.example.com/my": false,
388 "https://www.example.com/my/full/path": true,
390 for _, store := range clientStores {
391 context := Context{clients: store}
392 err := context.SaveClient(client)
394 t.Fatalf("Error saving client in %T: %s", store, err)
396 err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
398 t.Fatalf("Error saving endpoint in %T: %s", store, err)
400 err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
402 t.Fatalf("Error saving endpoint in %T: %s", store, err)
404 for candidate, expectation := range candidates {
405 result, err := context.CheckEndpoint(client.ID, candidate)
407 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
409 if result != expectation {
416 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
422 func TestClientChangeValidation(t *testing.T) {
424 change := ClientChange{}
425 if err := change.Validate(); err != ErrEmptyChange {
426 t.Errorf("Expected %s to give an error of %s, gave %s", "empty change", ErrEmptyChange, err)
428 names := map[string]error{
429 "a": ErrClientNameTooShort,
432 "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": ErrClientNameTooLong,
434 for name, expectation := range names {
435 change = ClientChange{Name: &name}
436 if err := change.Validate(); err != expectation {
437 t.Errorf("Expected %s to give an error of %s, gave %s", name, expectation, err)
441 for i := 0; i < 1025; i++ {
442 longPath = fmt.Sprintf("%s%d", longPath, i)
444 logos := map[string]error{
445 "https://www.example.com/" + longPath: ErrClientLogoTooLong,
446 "https://www.example.com/ab": nil,
447 "www.example.com/ab": ErrClientLogoNotURL,
448 "test": ErrClientLogoNotURL,
451 for logo, expectation := range logos {
452 change = ClientChange{Logo: &logo}
453 if err := change.Validate(); err != expectation {
454 t.Errorf("Expected %s to give an error of %s, gave %s", logo, expectation, err)
457 websites := map[string]error{
458 "https://www.example.com/" + longPath: ErrClientWebsiteTooLong,
459 "https://www.example.com/ab": nil,
460 "www.example.com/ab": ErrClientWebsiteNotURL,
461 "test": ErrClientWebsiteNotURL,
464 for website, expectation := range websites {
465 change = ClientChange{Website: &website}
466 if err := change.Validate(); err != expectation {
467 t.Errorf("Expected %s to give an error of %s, gave %s", website, expectation, err)
472 func TestVerifyClient(t *testing.T) {
474 memstore := NewMemstore()
480 Secret: "super secret!",
481 OwnerID: uuid.NewID(),
482 Name: "My test client",
483 Logo: "https://secondbit.org/logo.png",
484 Website: "https://secondbit.org/",
485 Type: "confidential",
487 err := context.SaveClient(client)
489 t.Fatal("Could not save client:", err)
491 publicClient := Client{
494 OwnerID: uuid.NewID(),
495 Name: "A public client",
496 Logo: "https://secondbit.org/logo.png",
497 Website: "https://secondbit.org/",
500 err = context.SaveClient(publicClient)
502 t.Fatal("Could not save client:", err)
505 // verifyClient with no auth header, no public clients
506 w := httptest.NewRecorder()
507 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
509 t.Fatal("Can't build request:", err)
511 resp, success := verifyClient(w, r, false, context)
513 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
516 t.Error("Expected nil client ID, got", resp)
518 if w.Code != http.StatusBadRequest {
519 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
521 expectedBody := `{"error":"unauthorized_client"}`
522 if expectedBody != strings.TrimSpace(w.Body.String()) {
523 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
526 // verifyClient with no auth header, public clients, empty client_id
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 resp, success = verifyClient(w, r, true, context)
534 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
537 t.Error("Expected nil client ID, got", resp)
539 if w.Code != http.StatusUnauthorized {
540 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
542 expectedBody = `{"error":"invalid_client"}`
543 if expectedBody != strings.TrimSpace(w.Body.String()) {
544 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
547 // verifyClient with auth header, no public clients, empty client id
548 w = httptest.NewRecorder()
549 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
551 t.Fatal("Can't build request:", err)
553 r.SetBasicAuth("", "no client ID set")
554 resp, success = verifyClient(w, r, false, context)
556 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
559 t.Error("Expected nil client ID, got", resp)
561 if w.Code != http.StatusUnauthorized {
562 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
564 expectedBody = `{"error":"invalid_client"}`
565 if expectedBody != strings.TrimSpace(w.Body.String()) {
566 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
568 if w.Header().Get("WWW-Authenticate") != "Basic" {
569 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
572 // verifyClient with auth header, public clients, empty client id
573 w = httptest.NewRecorder()
574 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
576 t.Fatal("Can't build request:", err)
578 r.SetBasicAuth("", "no client ID set")
579 resp, success = verifyClient(w, r, true, context)
581 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
584 t.Error("Expected nil client ID, got", resp)
586 if w.Code != http.StatusUnauthorized {
587 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
589 expectedBody = `{"error":"invalid_client"}`
590 if expectedBody != strings.TrimSpace(w.Body.String()) {
591 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
593 if w.Header().Get("WWW-Authenticate") != "Basic" {
594 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
597 // verifyClient with auth header, no public clients, invalid client ID
598 w = httptest.NewRecorder()
599 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
601 t.Fatal("Can't build request:", err)
603 r.SetBasicAuth("not an actual id", "invalid client ID set")
604 resp, success = verifyClient(w, r, false, context)
606 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
609 t.Error("Expected nil client ID, got", resp)
611 if w.Code != http.StatusUnauthorized {
612 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
614 expectedBody = `{"error":"invalid_client"}`
615 if expectedBody != strings.TrimSpace(w.Body.String()) {
616 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
618 if w.Header().Get("WWW-Authenticate") != "Basic" {
619 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
622 // verifyClient with auth header, public clients, invalid client ID
623 w = httptest.NewRecorder()
624 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
626 t.Fatal("Can't build request:", err)
628 r.SetBasicAuth("not an actual id", "invalid client ID set")
629 resp, success = verifyClient(w, r, true, context)
631 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
634 t.Error("Expected nil client ID, got", resp)
636 if w.Code != http.StatusUnauthorized {
637 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
639 expectedBody = `{"error":"invalid_client"}`
640 if expectedBody != strings.TrimSpace(w.Body.String()) {
641 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
643 if w.Header().Get("WWW-Authenticate") != "Basic" {
644 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
647 // verifyClient with auth header, no public clients, client ID valid but not in clientStore
648 w = httptest.NewRecorder()
649 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
651 t.Fatal("Can't build request:", err)
653 r.SetBasicAuth(uuid.NewID().String(), "non existent client ID set")
654 resp, success = verifyClient(w, r, false, context)
656 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
659 t.Error("Expected nil client ID, got", resp)
661 if w.Code != http.StatusUnauthorized {
662 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
664 expectedBody = `{"error":"invalid_client"}`
665 if expectedBody != strings.TrimSpace(w.Body.String()) {
666 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
668 if w.Header().Get("WWW-Authenticate") != "Basic" {
669 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
672 // verifyClient with auth header, public clients, client ID valid but not in clientStore
673 w = httptest.NewRecorder()
674 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
676 t.Fatal("Can't build request:", err)
678 r.SetBasicAuth(uuid.NewID().String(), "non existent client ID set")
679 resp, success = verifyClient(w, r, true, context)
681 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
684 t.Error("Expected nil client ID, got", resp)
686 if w.Code != http.StatusUnauthorized {
687 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
689 expectedBody = `{"error":"invalid_client"}`
690 if expectedBody != strings.TrimSpace(w.Body.String()) {
691 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
693 if w.Header().Get("WWW-Authenticate") != "Basic" {
694 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
697 // verifyClient with auth header, no public clients, client secret wrong
698 w = httptest.NewRecorder()
699 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
701 t.Fatal("Can't build request:", err)
703 r.SetBasicAuth(client.ID.String(), "not actually the secret")
704 resp, success = verifyClient(w, r, false, context)
706 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
709 t.Error("Expected nil client ID, got", resp)
711 if w.Code != http.StatusUnauthorized {
712 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
714 expectedBody = `{"error":"invalid_client"}`
715 if expectedBody != strings.TrimSpace(w.Body.String()) {
716 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
718 if w.Header().Get("WWW-Authenticate") != "Basic" {
719 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
722 // verifyClient with auth header, public clients, client secret wrong
723 w = httptest.NewRecorder()
724 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
726 t.Fatal("Can't build request:", err)
728 r.SetBasicAuth(client.ID.String(), "not actually the secret")
729 resp, success = verifyClient(w, r, true, context)
731 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
734 t.Error("Expected nil client ID, got", resp)
736 if w.Code != http.StatusUnauthorized {
737 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
739 expectedBody = `{"error":"invalid_client"}`
740 if expectedBody != strings.TrimSpace(w.Body.String()) {
741 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
743 if w.Header().Get("WWW-Authenticate") != "Basic" {
744 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
747 // verifyClient with no auth header, public clients, invalid client_id post form value
748 w = httptest.NewRecorder()
749 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
751 t.Fatal("Can't build request:", err)
753 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
754 params := url.Values{}
755 params.Set("client_id", "not an actual id")
756 body := bytes.NewBufferString(params.Encode())
757 r.Body = ioutil.NopCloser(body)
758 resp, success = verifyClient(w, r, true, context)
760 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
763 t.Error("Expected nil client ID, got", resp)
765 if w.Code != http.StatusUnauthorized {
766 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
768 expectedBody = `{"error":"invalid_client"}`
769 if expectedBody != strings.TrimSpace(w.Body.String()) {
770 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
773 // verifyClient with no auth header, public clients, client_id valid but not in clientStore
774 w = httptest.NewRecorder()
775 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
777 t.Fatal("Can't build request:", err)
779 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
780 params = url.Values{}
781 params.Set("client_id", uuid.NewID().String())
782 body = bytes.NewBufferString(params.Encode())
783 r.Body = ioutil.NopCloser(body)
784 resp, success = verifyClient(w, r, true, context)
786 t.Error("Expected verification to fail, but succeeded with client ID:", resp)
789 t.Error("Expected nil client ID, got", resp)
791 if w.Code != http.StatusUnauthorized {
792 t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code)
794 expectedBody = `{"error":"invalid_client"}`
795 if expectedBody != strings.TrimSpace(w.Body.String()) {
796 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String()))
799 // verifyClient with auth header, public clients, valid client_id and secret
800 w = httptest.NewRecorder()
801 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
803 t.Fatal("Can't build request:", err)
805 r.SetBasicAuth(client.ID.String(), client.Secret)
806 resp, success = verifyClient(w, r, true, context)
808 t.Error("Expected verification to succeed, but it failed")
810 if !client.ID.Equal(resp) {
811 t.Errorf("Expected client ID to be %s, got %s", client.ID, resp)
813 if w.Code != http.StatusOK {
814 t.Errorf("Expected status code of %d, got %d", http.StatusOK, w.Code)
816 if w.Body.String() != "" {
817 t.Errorf(`Expected empty body, got "%s"`, w.Body.String())
820 // verifyClient with auth header, no public clients, valid client_id and secret
821 w = httptest.NewRecorder()
822 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
824 t.Fatal("Can't build request:", err)
826 r.SetBasicAuth(client.ID.String(), client.Secret)
827 resp, success = verifyClient(w, r, true, context)
829 t.Error("Expected verification to succeed, but it failed")
831 if !client.ID.Equal(resp) {
832 t.Errorf("Expected client ID to be %s, got %s", client.ID, resp)
834 if w.Code != http.StatusOK {
835 t.Errorf("Expected status code of %d, got %d", http.StatusOK, w.Code)
837 if w.Body.String() != "" {
838 t.Errorf(`Expected empty body, got "%s"`, w.Body.String())
841 // verifyClient with no auth header, public clients, valid client_id and secret
842 w = httptest.NewRecorder()
843 r, err = http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
845 t.Fatal("Can't build request:", err)
847 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
848 params = url.Values{}
849 params.Set("client_id", publicClient.ID.String())
850 body = bytes.NewBufferString(params.Encode())
851 r.Body = ioutil.NopCloser(body)
852 resp, success = verifyClient(w, r, true, context)
854 t.Error("Expected verification to succeed, but it failed")
856 if !publicClient.ID.Equal(resp) {
857 t.Errorf("Expected client ID to be %s, got %s", publicClient.ID, resp)
859 if w.Code != http.StatusOK {
860 t.Errorf("Expected status code of %d, got %d", http.StatusOK, w.Code)
862 if w.Body.String() != "" {
863 t.Errorf(`Expected empty body, got "%s"`, w.Body.String())
867 func TestCreateClientHandler(t *testing.T) {
869 memstore := NewMemstore()
874 w := httptest.NewRecorder()
875 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/clients", nil)
877 t.Fatal("Can't build request:", err)
879 r.Header.Set("Content-Type", "application/json")
880 CreateClientHandler(w, r, c)
881 if w.Code != http.StatusUnauthorized {
882 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
884 expected := `{"errors":[{"error":"access_denied"}]}`
885 result := strings.TrimSpace(w.Body.String())
886 if result != expected {
887 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
889 w = httptest.NewRecorder()
890 r.Header.Set("Authorization", "Not basic at all...")
891 CreateClientHandler(w, r, c)
892 if w.Code != http.StatusUnauthorized {
893 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
895 expected = `{"errors":[{"error":"access_denied"}]}`
896 result = strings.TrimSpace(w.Body.String())
897 if result != expected {
898 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
900 w = httptest.NewRecorder()
901 r.Header.Set("Authorization", "Basic TotallyNotBase64Encoded")
902 CreateClientHandler(w, r, c)
903 if w.Code != http.StatusUnauthorized {
904 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
906 expected = `{"errors":[{"error":"access_denied"}]}`
907 result = strings.TrimSpace(w.Body.String())
908 if result != expected {
909 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
911 w = httptest.NewRecorder()
912 r.Header.Set("Authorization", "Basic dGhpc2hhc25vY29sb24=")
913 CreateClientHandler(w, r, c)
914 if w.Code != http.StatusUnauthorized {
915 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
917 expected = `{"errors":[{"error":"access_denied"}]}`
918 result = strings.TrimSpace(w.Body.String())
919 if result != expected {
920 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
925 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
927 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
930 LockedUntil: time.Time{},
932 PassphraseResetCreated: time.Time{},
934 LastSeen: time.Time{},
938 Value: "test@example.com",
939 ProfileID: profile.ID,
941 LastUsed: time.Time{},
943 w = httptest.NewRecorder()
944 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
945 CreateClientHandler(w, r, c)
946 if w.Code != http.StatusUnauthorized {
947 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
949 expected = `{"errors":[{"error":"access_denied"}]}`
950 result = strings.TrimSpace(w.Body.String())
951 if result != expected {
952 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
954 err = c.SaveProfile(profile)
956 t.Error("Error saving profile:", err)
958 err = c.AddLogin(login)
960 t.Error("Error adding login:", err)
962 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
963 type testStruct struct {
968 tests := []testStruct{
969 {``, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/"}}}},
970 {`{}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
971 {`{"type":"notarealtype"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
972 {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrOverflow, Field: "/name"}}}},
973 {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrInsufficient, Field: "/name"}}}},
974 {`{"type":"public"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/name"}}}},
975 {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrOverflow, Field: "/name"}}}},
976 {`{"type":"public","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInsufficient, Field: "/name"}}}},
977 {`{"name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}}}},
978 {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}}}},
979 {`{"type":"public","name":"My Client"}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}},
980 {`{"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"}}}},
981 {`{"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"}}}},
982 {`{"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"}}}},
984 for pos, test := range tests {
985 t.Logf("Test #%d: `%s`", pos, test.request)
986 w = httptest.NewRecorder()
987 body := bytes.NewBufferString(test.request)
988 r.Body = ioutil.NopCloser(body)
989 CreateClientHandler(w, r, c)
990 if w.Code != test.code {
991 t.Errorf("Expected response code to be %d, got %d", test.code, w.Code)
993 t.Logf("Response: %s", w.Body.String())
995 err = json.Unmarshal(w.Body.Bytes(), &res)
997 t.Error("Unexpected error unmarshalling response:", err)
999 fillInServerGenerated(test.resp, res)
1000 success, field, expectation, result := compareResponses(test.resp, res)
1002 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)