auth

Paddy 2014-09-18 Parent:0b86c2d3ec75 Child:022ce4262922

41:113ccb15b919 Browse Files

Added validation for clients, split endpoints out. Split endpoints out into their own type and added associated methods to the ClientStores, so now each client can have more than one redirect endpoint. Added unit testing for endpoint methods. Added validation code to validate client changes.

client.go client_test.go memstore.go

     1.1 --- a/client.go	Thu Sep 18 19:34:18 2014 -0400
     1.2 +++ b/client.go	Thu Sep 18 22:13:22 2014 -0400
     1.3 @@ -2,35 +2,42 @@
     1.4  
     1.5  import (
     1.6  	"errors"
     1.7 +	"net/url"
     1.8 +	"time"
     1.9  
    1.10 +	"strings"
    1.11  	"secondbit.org/uuid"
    1.12  )
    1.13  
    1.14  var (
    1.15  	ErrClientNotFound      = errors.New("Client not found in ClientStore.")
    1.16  	ErrClientAlreadyExists = errors.New("Client already exists in ClientStore.")
    1.17 +
    1.18 +	ErrClientNameTooShort    = errors.New("Client name must be at least 2 characters.")
    1.19 +	ErrClientNameTooLong     = errors.New("Client name must be at most 32 characters.")
    1.20 +	ErrClientLogoTooShort    = errors.New("Client logo URL must be at least 12 characters.")
    1.21 +	ErrClientLogoTooLong     = errors.New("Client logo must be at most 1024 characters.")
    1.22 +	ErrClientWebsiteTooShort = errors.New("Client website URL must be at least 12 characters.")
    1.23 +	ErrClientWebsiteTooLong  = errors.New("Client website must be at most 1024 characters.")
    1.24  )
    1.25  
    1.26  // Client represents a client that grants access
    1.27  // to the auth server, exchanging grants for tokens,
    1.28  // and tokens for access.
    1.29  type Client struct {
    1.30 -	ID          uuid.ID
    1.31 -	Secret      string
    1.32 -	RedirectURI string
    1.33 -	OwnerID     uuid.ID
    1.34 -	Name        string
    1.35 -	Logo        string
    1.36 -	Website     string
    1.37 +	ID      uuid.ID
    1.38 +	Secret  string
    1.39 +	OwnerID uuid.ID
    1.40 +	Name    string
    1.41 +	Logo    string
    1.42 +	Website string
    1.43 +	Type    string
    1.44  }
    1.45  
    1.46  func (c *Client) ApplyChange(change ClientChange) {
    1.47  	if change.Secret != nil {
    1.48  		c.Secret = *change.Secret
    1.49  	}
    1.50 -	if change.RedirectURI != nil {
    1.51 -		c.RedirectURI = *change.RedirectURI
    1.52 -	}
    1.53  	if change.OwnerID != nil {
    1.54  		c.OwnerID = change.OwnerID
    1.55  	}
    1.56 @@ -46,19 +53,56 @@
    1.57  }
    1.58  
    1.59  type ClientChange struct {
    1.60 -	Secret      *string
    1.61 -	RedirectURI *string
    1.62 -	OwnerID     uuid.ID
    1.63 -	Name        *string
    1.64 -	Logo        *string
    1.65 -	Website     *string
    1.66 +	Secret  *string
    1.67 +	OwnerID uuid.ID
    1.68 +	Name    *string
    1.69 +	Logo    *string
    1.70 +	Website *string
    1.71  }
    1.72  
    1.73  func (c ClientChange) Validate() error {
    1.74 -	// TODO: validate client changes
    1.75 +	if c.Name != nil && len(*c.Name) < 2 {
    1.76 +		return ErrClientNameTooShort
    1.77 +	}
    1.78 +	if c.Name != nil && len(*c.Name) > 32 {
    1.79 +		return ErrClientNameTooLong
    1.80 +	}
    1.81 +	if c.Logo != nil && len(*c.Logo) > 1024 {
    1.82 +		return ErrClientLogoTooLong
    1.83 +	}
    1.84 +	if c.Logo != nil && len(*c.Logo) > 0 && len(*c.Logo) < 12 {
    1.85 +		return ErrClientLogoTooShort
    1.86 +	}
    1.87 +	if c.Website != nil && len(*c.Website) > 140 {
    1.88 +		return ErrClientWebsiteTooLong
    1.89 +	}
    1.90 +	if c.Website != nil && len(*c.Website) > 0 && len(*c.Website) < 12 {
    1.91 +		return ErrClientWebsiteTooShort
    1.92 +	}
    1.93  	return nil
    1.94  }
    1.95  
    1.96 +type Endpoint struct {
    1.97 +	ID       uuid.ID
    1.98 +	ClientID uuid.ID
    1.99 +	URI      url.URL
   1.100 +	Added    time.Time
   1.101 +}
   1.102 +
   1.103 +type sortedEndpoints []Endpoint
   1.104 +
   1.105 +func (s sortedEndpoints) Len() int {
   1.106 +	return len(s)
   1.107 +}
   1.108 +
   1.109 +func (s sortedEndpoints) Less(i, j int) bool {
   1.110 +	return s[i].Added.Before(s[j].Added)
   1.111 +}
   1.112 +
   1.113 +func (s sortedEndpoints) Swap(i, j int) {
   1.114 +	s[i], s[j] = s[j], s[i]
   1.115 +}
   1.116 +
   1.117  // ClientStore abstracts the storage interface for
   1.118  // storing and retrieving Clients.
   1.119  type ClientStore interface {
   1.120 @@ -67,6 +111,11 @@
   1.121  	UpdateClient(id uuid.ID, change ClientChange) error
   1.122  	DeleteClient(id uuid.ID) error
   1.123  	ListClientsByOwner(ownerID uuid.ID, num, offset int) ([]Client, error)
   1.124 +
   1.125 +	AddEndpoint(client uuid.ID, endpoint Endpoint) error
   1.126 +	RemoveEndpoint(client, endpoint uuid.ID) error
   1.127 +	CheckEndpoint(client uuid.ID, endpoint string) (bool, error)
   1.128 +	ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error)
   1.129  }
   1.130  
   1.131  func (m *Memstore) GetClient(id uuid.ID) (Client, error) {
   1.132 @@ -142,3 +191,43 @@
   1.133  	}
   1.134  	return clients, nil
   1.135  }
   1.136 +
   1.137 +func (m *Memstore) AddEndpoint(client uuid.ID, endpoint Endpoint) error {
   1.138 +	m.endpointLock.Lock()
   1.139 +	defer m.endpointLock.Unlock()
   1.140 +	m.endpoints[client.String()] = append(m.endpoints[client.String()], endpoint)
   1.141 +	return nil
   1.142 +}
   1.143 +
   1.144 +func (m *Memstore) RemoveEndpoint(client, endpoint uuid.ID) error {
   1.145 +	m.endpointLock.Lock()
   1.146 +	defer m.endpointLock.Unlock()
   1.147 +	pos := -1
   1.148 +	for p, item := range m.endpoints[client.String()] {
   1.149 +		if item.ID.Equal(endpoint) {
   1.150 +			pos = p
   1.151 +			break
   1.152 +		}
   1.153 +	}
   1.154 +	if pos >= 0 {
   1.155 +		m.endpoints[client.String()] = append(m.endpoints[client.String()][:pos], m.endpoints[client.String()][pos+1:]...)
   1.156 +	}
   1.157 +	return nil
   1.158 +}
   1.159 +
   1.160 +func (m *Memstore) CheckEndpoint(client uuid.ID, endpoint string) (bool, error) {
   1.161 +	m.endpointLock.RLock()
   1.162 +	defer m.endpointLock.RUnlock()
   1.163 +	for _, candidate := range m.endpoints[client.String()] {
   1.164 +		if strings.HasPrefix(endpoint, candidate.URI.String()) {
   1.165 +			return true, nil
   1.166 +		}
   1.167 +	}
   1.168 +	return false, nil
   1.169 +}
   1.170 +
   1.171 +func (m *Memstore) ListEndpoints(client uuid.ID, num, offset int) ([]Endpoint, error) {
   1.172 +	m.endpointLock.RLock()
   1.173 +	defer m.endpointLock.RUnlock()
   1.174 +	return m.endpoints[client.String()], nil
   1.175 +}
     2.1 --- a/client_test.go	Thu Sep 18 19:34:18 2014 -0400
     2.2 +++ b/client_test.go	Thu Sep 18 22:13:22 2014 -0400
     2.3 @@ -2,14 +2,16 @@
     2.4  
     2.5  import (
     2.6  	"fmt"
     2.7 +	"net/url"
     2.8  	"testing"
     2.9 +	"time"
    2.10  
    2.11 +	"sort"
    2.12  	"secondbit.org/uuid"
    2.13  )
    2.14  
    2.15  const (
    2.16  	clientChangeSecret = 1 << iota
    2.17 -	clientChangeRedirectURI
    2.18  	clientChangeOwnerID
    2.19  	clientChangeName
    2.20  	clientChangeLogo
    2.21 @@ -25,9 +27,6 @@
    2.22  	if client1.Secret != client2.Secret {
    2.23  		return false, "secret", client1.Secret, client2.Secret
    2.24  	}
    2.25 -	if client1.RedirectURI != client2.RedirectURI {
    2.26 -		return false, "redirect URI", client1.RedirectURI, client2.RedirectURI
    2.27 -	}
    2.28  	if !client1.OwnerID.Equal(client2.OwnerID) {
    2.29  		return false, "owner ID", client1.OwnerID, client2.OwnerID
    2.30  	}
    2.31 @@ -40,84 +39,196 @@
    2.32  	if client1.Website != client2.Website {
    2.33  		return false, "website", client1.Website, client2.Website
    2.34  	}
    2.35 +	if client1.Type != client2.Type {
    2.36 +		return false, "type", client1.Type, client2.Type
    2.37 +	}
    2.38 +	return true, "", nil, nil
    2.39 +}
    2.40 +
    2.41 +func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) {
    2.42 +	if !endpoint1.ID.Equal(endpoint2.ID) {
    2.43 +		return false, "ID", endpoint1.ID, endpoint2.ID
    2.44 +	}
    2.45 +	if !endpoint1.ClientID.Equal(endpoint2.ClientID) {
    2.46 +		return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID
    2.47 +	}
    2.48 +	if !endpoint1.Added.Equal(endpoint2.Added) {
    2.49 +		return false, "Added", endpoint1.Added, endpoint2.Added
    2.50 +	}
    2.51 +	if endpoint1.URI.String() != endpoint2.URI.String() {
    2.52 +		return false, "URI", endpoint1.URI, endpoint2.URI
    2.53 +	}
    2.54  	return true, "", nil, nil
    2.55  }
    2.56  
    2.57  func TestClientStoreSuccess(t *testing.T) {
    2.58  	t.Parallel()
    2.59  	client := Client{
    2.60 -		ID:          uuid.NewID(),
    2.61 -		Secret:      "secret",
    2.62 -		RedirectURI: "redirectURI",
    2.63 -		OwnerID:     uuid.NewID(),
    2.64 -		Name:        "name",
    2.65 -		Logo:        "logo",
    2.66 -		Website:     "website",
    2.67 +		ID:      uuid.NewID(),
    2.68 +		Secret:  "secret",
    2.69 +		OwnerID: uuid.NewID(),
    2.70 +		Name:    "name",
    2.71 +		Logo:    "logo",
    2.72 +		Website: "website",
    2.73  	}
    2.74  	for _, store := range clientStores {
    2.75  		err := store.SaveClient(client)
    2.76  		if err != nil {
    2.77 -			t.Errorf("Error saving client to %T: %s", store, err)
    2.78 +			t.Fatalf("Error saving client to %T: %s", store, err)
    2.79  		}
    2.80  		err = store.SaveClient(client)
    2.81  		if err != ErrClientAlreadyExists {
    2.82 -			t.Errorf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
    2.83 +			t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
    2.84  		}
    2.85  		retrieved, err := store.GetClient(client.ID)
    2.86  		if err != nil {
    2.87 -			t.Errorf("Error retrieving client from %T: %s", store, err)
    2.88 +			t.Fatalf("Error retrieving client from %T: %s", store, err)
    2.89  		}
    2.90  		success, field, expectation, result := compareClients(client, retrieved)
    2.91  		if !success {
    2.92 -			t.Errorf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
    2.93 +			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
    2.94  		}
    2.95  		clients, err := store.ListClientsByOwner(client.OwnerID, 25, 0)
    2.96  		if err != nil {
    2.97 -			t.Errorf("Error retrieving clients by owner from %T: %s", store, err)
    2.98 +			t.Fatalf("Error retrieving clients by owner from %T: %s", store, err)
    2.99  		}
   2.100  		if len(clients) != 1 {
   2.101 -			t.Errorf("Expected 1 client in response from %T, got %+v", store, clients)
   2.102 +			t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients)
   2.103  		}
   2.104  		success, field, expectation, result = compareClients(client, clients[0])
   2.105  		if !success {
   2.106 -			t.Errorf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
   2.107 +			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
   2.108  		}
   2.109  		err = store.DeleteClient(client.ID)
   2.110  		if err != nil {
   2.111 -			t.Errorf("Error deleting client from %T: %s", store, err)
   2.112 +			t.Fatalf("Error deleting client from %T: %s", store, err)
   2.113  		}
   2.114  		err = store.DeleteClient(client.ID)
   2.115  		if err != ErrClientNotFound {
   2.116 -			t.Errorf("Expected ErrClientNotFound, got %s from %T", err, store)
   2.117 +			t.Fatalf("Expected ErrClientNotFound, got %s from %T", err, store)
   2.118  		}
   2.119  		retrieved, err = store.GetClient(client.ID)
   2.120  		if err != ErrClientNotFound {
   2.121 -			t.Errorf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
   2.122 +			t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
   2.123  		}
   2.124  		clients, err = store.ListClientsByOwner(client.OwnerID, 25, 0)
   2.125  		if err != nil {
   2.126 -			t.Errorf("Error listing clients by owner from %T: %s", store, err)
   2.127 +			t.Fatalf("Error listing clients by owner from %T: %s", store, err)
   2.128  		}
   2.129  		if len(clients) != 0 {
   2.130 -			t.Errorf("Expected 0 clients in response from %T, got %+v", store, clients)
   2.131 +			t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients)
   2.132 +		}
   2.133 +	}
   2.134 +}
   2.135 +
   2.136 +func TestEndpointStoreSuccess(t *testing.T) {
   2.137 +	t.Parallel()
   2.138 +	client := Client{
   2.139 +		ID:      uuid.NewID(),
   2.140 +		Secret:  "secret",
   2.141 +		OwnerID: uuid.NewID(),
   2.142 +		Name:    "name",
   2.143 +		Logo:    "logo",
   2.144 +		Website: "website",
   2.145 +	}
   2.146 +	uri1, _ := url.Parse("https://www.example.com/")
   2.147 +	uri2, _ := url.Parse("https://www.example.com/my/full/path")
   2.148 +	endpoint1 := Endpoint{
   2.149 +		ID:       uuid.NewID(),
   2.150 +		ClientID: client.ID,
   2.151 +		Added:    time.Now(),
   2.152 +		URI:      *uri1,
   2.153 +	}
   2.154 +	endpoint2 := Endpoint{
   2.155 +		ID:       uuid.NewID(),
   2.156 +		ClientID: client.ID,
   2.157 +		Added:    time.Now(),
   2.158 +		URI:      *uri2,
   2.159 +	}
   2.160 +	for _, store := range clientStores {
   2.161 +		err := store.SaveClient(client)
   2.162 +		if err != nil {
   2.163 +			t.Fatalf("Error saving client to %T: %s", store, err)
   2.164 +		}
   2.165 +		err = store.AddEndpoint(client.ID, endpoint1)
   2.166 +		if err != nil {
   2.167 +			t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
   2.168 +		}
   2.169 +		endpoints, err := store.ListEndpoints(client.ID, 10, 0)
   2.170 +		if err != nil {
   2.171 +			t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
   2.172 +		}
   2.173 +		if len(endpoints) != 1 {
   2.174 +			t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
   2.175 +		}
   2.176 +		success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0])
   2.177 +		if !success {
   2.178 +			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
   2.179 +		}
   2.180 +		err = store.AddEndpoint(client.ID, endpoint2)
   2.181 +		if err != nil {
   2.182 +			t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
   2.183 +		}
   2.184 +		endpoints, err = store.ListEndpoints(client.ID, 10, 0)
   2.185 +		if err != nil {
   2.186 +			t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
   2.187 +		}
   2.188 +		if len(endpoints) != 2 {
   2.189 +			t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store)
   2.190 +		}
   2.191 +		sortedEnd := sortedEndpoints(endpoints)
   2.192 +		sort.Sort(sortedEnd)
   2.193 +		endpoints = []Endpoint(sortedEnd)
   2.194 +		success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0])
   2.195 +		if !success {
   2.196 +			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
   2.197 +		}
   2.198 +		success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1])
   2.199 +		if !success {
   2.200 +			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
   2.201 +		}
   2.202 +		err = store.RemoveEndpoint(client.ID, endpoint1.ID)
   2.203 +		if err != nil {
   2.204 +			t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
   2.205 +		}
   2.206 +		endpoints, err = store.ListEndpoints(client.ID, 10, 0)
   2.207 +		if err != nil {
   2.208 +			t.Fatalf("Error listing endpoints in %T: %s", store, err)
   2.209 +		}
   2.210 +		if len(endpoints) != 1 {
   2.211 +			t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
   2.212 +		}
   2.213 +		success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0])
   2.214 +		if !success {
   2.215 +			t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
   2.216 +		}
   2.217 +		err = store.RemoveEndpoint(client.ID, endpoint2.ID)
   2.218 +		if err != nil {
   2.219 +			t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
   2.220 +		}
   2.221 +		endpoints, err = store.ListEndpoints(client.ID, 10, 0)
   2.222 +		if err != nil {
   2.223 +			t.Fatalf("Error listing endpoints in %T: %s", store, err)
   2.224 +		}
   2.225 +		if len(endpoints) != 0 {
   2.226 +			t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store)
   2.227  		}
   2.228  	}
   2.229  }
   2.230  
   2.231  func TestClientUpdates(t *testing.T) {
   2.232  	t.Parallel()
   2.233 -	variations := 1 << 10
   2.234 +	variations := 1 << 5
   2.235  	client := Client{
   2.236 -		ID:          uuid.NewID(),
   2.237 -		Secret:      "secret",
   2.238 -		RedirectURI: "redirectURI",
   2.239 -		OwnerID:     uuid.NewID(),
   2.240 -		Name:        "name",
   2.241 -		Logo:        "logo",
   2.242 -		Website:     "website",
   2.243 +		ID:      uuid.NewID(),
   2.244 +		Secret:  "secret",
   2.245 +		OwnerID: uuid.NewID(),
   2.246 +		Name:    "name",
   2.247 +		Logo:    "logo",
   2.248 +		Website: "website",
   2.249  	}
   2.250  	for i := 0; i < variations; i++ {
   2.251 -		var secret, redirectURI, name, logo, website string
   2.252 +		var secret, name, logo, website string
   2.253  		change := ClientChange{}
   2.254  		expectation := client
   2.255  		result := client
   2.256 @@ -126,11 +237,6 @@
   2.257  			change.Secret = &secret
   2.258  			expectation.Secret = secret
   2.259  		}
   2.260 -		if i&clientChangeRedirectURI != 0 {
   2.261 -			redirectURI = fmt.Sprintf("redirect-uri-%d", i)
   2.262 -			change.RedirectURI = &redirectURI
   2.263 -			expectation.RedirectURI = redirectURI
   2.264 -		}
   2.265  		if i&clientChangeOwnerID != 0 {
   2.266  			change.OwnerID = uuid.NewID()
   2.267  			expectation.OwnerID = change.OwnerID
   2.268 @@ -153,33 +259,95 @@
   2.269  		result.ApplyChange(change)
   2.270  		match, field, expected, got := compareClients(expectation, result)
   2.271  		if !match {
   2.272 -			t.Errorf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
   2.273 +			t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
   2.274  		}
   2.275  		for _, store := range clientStores {
   2.276  			err := store.SaveClient(client)
   2.277  			if err != nil {
   2.278 -				t.Errorf("Error saving client in %T: %s", store, err)
   2.279 +				t.Fatalf("Error saving client in %T: %s", store, err)
   2.280  			}
   2.281  			err = store.UpdateClient(client.ID, change)
   2.282  			if err != nil {
   2.283 -				t.Errorf("Error updating client in %T: %s", store, err)
   2.284 +				t.Fatalf("Error updating client in %T: %s", store, err)
   2.285  			}
   2.286  			retrieved, err := store.GetClient(client.ID)
   2.287  			if err != nil {
   2.288 -				t.Errorf("Error getting profile from %T: %s", store, err)
   2.289 +				t.Fatalf("Error getting profile from %T: %s", store, err)
   2.290  			}
   2.291  			match, field, expected, got = compareClients(expectation, retrieved)
   2.292  			if !match {
   2.293 -				t.Errorf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
   2.294 +				t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
   2.295  			}
   2.296  			err = store.DeleteClient(client.ID)
   2.297  			if err != nil {
   2.298 -				t.Errorf("Error deleting client from %T: %s", store, err)
   2.299 +				t.Fatalf("Error deleting client from %T: %s", store, err)
   2.300  			}
   2.301  			err = store.UpdateClient(client.ID, change)
   2.302  			if err != ErrClientNotFound {
   2.303 -				t.Errorf("Expected ErrClientNotFound, got %v from %T", err, store)
   2.304 +				t.Fatalf("Expected ErrClientNotFound, got %v from %T", err, store)
   2.305  			}
   2.306  		}
   2.307  	}
   2.308  }
   2.309 +
   2.310 +func TestClientEndpointChecks(t *testing.T) {
   2.311 +	t.Parallel()
   2.312 +	client := Client{
   2.313 +		ID:      uuid.NewID(),
   2.314 +		Secret:  "secret",
   2.315 +		OwnerID: uuid.NewID(),
   2.316 +		Name:    "name",
   2.317 +		Logo:    "logo",
   2.318 +		Website: "website",
   2.319 +	}
   2.320 +	uri1, _ := url.Parse("https://www.example.com/first")
   2.321 +	uri2, _ := url.Parse("https://www.example.com/my/full/path")
   2.322 +	endpoint1 := Endpoint{
   2.323 +		ID:       uuid.NewID(),
   2.324 +		ClientID: client.ID,
   2.325 +		Added:    time.Now(),
   2.326 +		URI:      *uri1,
   2.327 +	}
   2.328 +	endpoint2 := Endpoint{
   2.329 +		ID:       uuid.NewID(),
   2.330 +		ClientID: client.ID,
   2.331 +		Added:    time.Now(),
   2.332 +		URI:      *uri2,
   2.333 +	}
   2.334 +	candidates := map[string]bool{
   2.335 +		"https://www.example.com/":                 false,
   2.336 +		"https://www.example.com/first":            true,
   2.337 +		"https://www.example.com/first/extra/path": true,
   2.338 +		"https://www.example.com/my":               false,
   2.339 +		"https://www.example.com/my/full/path":     true,
   2.340 +	}
   2.341 +	for _, store := range clientStores {
   2.342 +		err := store.SaveClient(client)
   2.343 +		if err != nil {
   2.344 +			t.Fatalf("Error saving client in %T: %s", store, err)
   2.345 +		}
   2.346 +		err = store.AddEndpoint(client.ID, endpoint1)
   2.347 +		if err != nil {
   2.348 +			t.Fatalf("Error saving endpoint in %T: %s", store, err)
   2.349 +		}
   2.350 +		err = store.AddEndpoint(client.ID, endpoint2)
   2.351 +		if err != nil {
   2.352 +			t.Fatalf("Error saving endpoint in %T: %s", store, err)
   2.353 +		}
   2.354 +		for candidate, expectation := range candidates {
   2.355 +			result, err := store.CheckEndpoint(client.ID, candidate)
   2.356 +			if err != nil {
   2.357 +				t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
   2.358 +			}
   2.359 +			if result != expectation {
   2.360 +				expectStr := "no"
   2.361 +				resultStr := "a"
   2.362 +				if expectation {
   2.363 +					expectStr = "a"
   2.364 +					resultStr = "no"
   2.365 +				}
   2.366 +				t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
   2.367 +			}
   2.368 +		}
   2.369 +	}
   2.370 +}
     3.1 --- a/memstore.go	Thu Sep 18 19:34:18 2014 -0400
     3.2 +++ b/memstore.go	Thu Sep 18 22:13:22 2014 -0400
     3.3 @@ -19,6 +19,9 @@
     3.4  	profileClientLookup map[string][]uuid.ID
     3.5  	clientLock          sync.RWMutex
     3.6  
     3.7 +	endpoints    map[string][]Endpoint
     3.8 +	endpointLock sync.RWMutex
     3.9 +
    3.10  	profiles    map[string]Profile
    3.11  	profileLock sync.RWMutex
    3.12  }
    3.13 @@ -31,6 +34,7 @@
    3.14  		grants:              map[string]Grant{},
    3.15  		clients:             map[string]Client{},
    3.16  		profileClientLookup: map[string][]uuid.ID{},
    3.17 +		endpoints:           map[string][]Endpoint{},
    3.18  		profiles:            map[string]Profile{},
    3.19  	}
    3.20  }