auth

Paddy 2015-03-24 Parent:77db7c65216c Child:3e8964a914ef

152:de5e09680f6b Go to Latest

auth/client_test.go

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.

History
paddy@31 1 package auth
paddy@31 2
paddy@31 3 import (
paddy@113 4 "bytes"
paddy@116 5 "encoding/json"
paddy@39 6 "fmt"
paddy@139 7 "github.com/gorilla/mux"
paddy@113 8 "io/ioutil"
paddy@113 9 "net/http"
paddy@113 10 "net/http/httptest"
paddy@41 11 "net/url"
paddy@82 12 "sort"
paddy@113 13 "strings"
paddy@31 14 "testing"
paddy@41 15 "time"
paddy@31 16
paddy@107 17 "code.secondbit.org/uuid.hg"
paddy@31 18 )
paddy@31 19
paddy@39 20 const (
paddy@39 21 clientChangeSecret = 1 << iota
paddy@39 22 clientChangeOwnerID
paddy@39 23 clientChangeName
paddy@39 24 clientChangeLogo
paddy@39 25 clientChangeWebsite
paddy@39 26 )
paddy@39 27
paddy@151 28 func init() {
paddy@151 29 p, err := NewPostgres("dbname=testdb sslmode=disable")
paddy@151 30 if err != nil {
paddy@151 31 panic(err)
paddy@151 32 }
paddy@151 33 if !testing.Short() {
paddy@151 34 clientStores = append(clientStores, &p)
paddy@151 35 }
paddy@151 36 }
paddy@151 37
paddy@57 38 var clientStores = []clientStore{NewMemstore()}
paddy@31 39
paddy@33 40 func compareClients(client1, client2 Client) (success bool, field string, val1, val2 interface{}) {
paddy@33 41 if !client1.ID.Equal(client2.ID) {
paddy@33 42 return false, "ID", client1.ID, client2.ID
paddy@33 43 }
paddy@33 44 if client1.Secret != client2.Secret {
paddy@33 45 return false, "secret", client1.Secret, client2.Secret
paddy@33 46 }
paddy@33 47 if !client1.OwnerID.Equal(client2.OwnerID) {
paddy@33 48 return false, "owner ID", client1.OwnerID, client2.OwnerID
paddy@33 49 }
paddy@33 50 if client1.Name != client2.Name {
paddy@33 51 return false, "name", client1.Name, client2.Name
paddy@33 52 }
paddy@33 53 if client1.Logo != client2.Logo {
paddy@33 54 return false, "logo", client1.Logo, client2.Logo
paddy@33 55 }
paddy@33 56 if client1.Website != client2.Website {
paddy@33 57 return false, "website", client1.Website, client2.Website
paddy@33 58 }
paddy@41 59 if client1.Type != client2.Type {
paddy@41 60 return false, "type", client1.Type, client2.Type
paddy@41 61 }
paddy@41 62 return true, "", nil, nil
paddy@41 63 }
paddy@41 64
paddy@41 65 func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) {
paddy@41 66 if !endpoint1.ID.Equal(endpoint2.ID) {
paddy@41 67 return false, "ID", endpoint1.ID, endpoint2.ID
paddy@41 68 }
paddy@41 69 if !endpoint1.ClientID.Equal(endpoint2.ClientID) {
paddy@41 70 return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID
paddy@41 71 }
paddy@41 72 if !endpoint1.Added.Equal(endpoint2.Added) {
paddy@41 73 return false, "Added", endpoint1.Added, endpoint2.Added
paddy@41 74 }
paddy@116 75 if endpoint1.URI != endpoint2.URI {
paddy@41 76 return false, "URI", endpoint1.URI, endpoint2.URI
paddy@41 77 }
paddy@33 78 return true, "", nil, nil
paddy@33 79 }
paddy@33 80
paddy@31 81 func TestClientStoreSuccess(t *testing.T) {
paddy@36 82 t.Parallel()
paddy@31 83 client := Client{
paddy@41 84 ID: uuid.NewID(),
paddy@41 85 Secret: "secret",
paddy@41 86 OwnerID: uuid.NewID(),
paddy@41 87 Name: "name",
paddy@41 88 Logo: "logo",
paddy@41 89 Website: "website",
paddy@31 90 }
paddy@31 91 for _, store := range clientStores {
paddy@116 92 context := Context{clients: store}
paddy@116 93 err := context.SaveClient(client)
paddy@31 94 if err != nil {
paddy@41 95 t.Fatalf("Error saving client to %T: %s", store, err)
paddy@31 96 }
paddy@116 97 err = context.SaveClient(client)
paddy@33 98 if err != ErrClientAlreadyExists {
paddy@41 99 t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
paddy@33 100 }
paddy@116 101 retrieved, err := context.GetClient(client.ID)
paddy@31 102 if err != nil {
paddy@41 103 t.Fatalf("Error retrieving client from %T: %s", store, err)
paddy@31 104 }
paddy@33 105 success, field, expectation, result := compareClients(client, retrieved)
paddy@33 106 if !success {
paddy@41 107 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@33 108 }
paddy@116 109 clients, err := context.ListClientsByOwner(client.OwnerID, 25, 0)
paddy@31 110 if err != nil {
paddy@41 111 t.Fatalf("Error retrieving clients by owner from %T: %s", store, err)
paddy@31 112 }
paddy@31 113 if len(clients) != 1 {
paddy@41 114 t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients)
paddy@31 115 }
paddy@33 116 success, field, expectation, result = compareClients(client, clients[0])
paddy@33 117 if !success {
paddy@41 118 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@33 119 }
paddy@151 120 deleted := true
paddy@151 121 err = context.UpdateClient(client.ID, ClientChange{Deleted: &deleted})
paddy@31 122 if err != nil {
paddy@41 123 t.Fatalf("Error deleting client from %T: %s", store, err)
paddy@31 124 }
paddy@116 125 retrieved, err = context.GetClient(client.ID)
paddy@31 126 if err != ErrClientNotFound {
paddy@41 127 t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
paddy@31 128 }
paddy@116 129 clients, err = context.ListClientsByOwner(client.OwnerID, 25, 0)
paddy@31 130 if err != nil {
paddy@41 131 t.Fatalf("Error listing clients by owner from %T: %s", store, err)
paddy@31 132 }
paddy@31 133 if len(clients) != 0 {
paddy@41 134 t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients)
paddy@41 135 }
paddy@41 136 }
paddy@41 137 }
paddy@41 138
paddy@41 139 func TestEndpointStoreSuccess(t *testing.T) {
paddy@41 140 t.Parallel()
paddy@41 141 client := Client{
paddy@41 142 ID: uuid.NewID(),
paddy@41 143 Secret: "secret",
paddy@41 144 OwnerID: uuid.NewID(),
paddy@41 145 Name: "name",
paddy@41 146 Logo: "logo",
paddy@41 147 Website: "website",
paddy@41 148 }
paddy@41 149 endpoint1 := Endpoint{
paddy@41 150 ID: uuid.NewID(),
paddy@41 151 ClientID: client.ID,
paddy@149 152 Added: time.Now().Round(time.Millisecond),
paddy@116 153 URI: "https://www.example.com/",
paddy@41 154 }
paddy@41 155 endpoint2 := Endpoint{
paddy@41 156 ID: uuid.NewID(),
paddy@41 157 ClientID: client.ID,
paddy@149 158 Added: time.Now().Round(time.Millisecond),
paddy@116 159 URI: "https://www.example.com/my/full/path",
paddy@41 160 }
paddy@41 161 for _, store := range clientStores {
paddy@116 162 context := Context{clients: store}
paddy@116 163 err := context.SaveClient(client)
paddy@41 164 if err != nil {
paddy@41 165 t.Fatalf("Error saving client to %T: %s", store, err)
paddy@41 166 }
paddy@151 167 err = context.AddEndpoints([]Endpoint{endpoint1})
paddy@41 168 if err != nil {
paddy@41 169 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
paddy@41 170 }
paddy@116 171 endpoints, err := context.ListEndpoints(client.ID, 10, 0)
paddy@41 172 if err != nil {
paddy@41 173 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
paddy@41 174 }
paddy@41 175 if len(endpoints) != 1 {
paddy@41 176 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
paddy@41 177 }
paddy@41 178 success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0])
paddy@41 179 if !success {
paddy@41 180 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@41 181 }
paddy@151 182 err = context.AddEndpoints([]Endpoint{endpoint2})
paddy@41 183 if err != nil {
paddy@41 184 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
paddy@41 185 }
paddy@116 186 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
paddy@41 187 if err != nil {
paddy@41 188 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
paddy@41 189 }
paddy@41 190 if len(endpoints) != 2 {
paddy@41 191 t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store)
paddy@41 192 }
paddy@41 193 sortedEnd := sortedEndpoints(endpoints)
paddy@41 194 sort.Sort(sortedEnd)
paddy@41 195 endpoints = []Endpoint(sortedEnd)
paddy@41 196 success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0])
paddy@41 197 if !success {
paddy@41 198 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@41 199 }
paddy@41 200 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1])
paddy@41 201 if !success {
paddy@41 202 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@41 203 }
paddy@116 204 err = context.RemoveEndpoint(client.ID, endpoint1.ID)
paddy@41 205 if err != nil {
paddy@41 206 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
paddy@41 207 }
paddy@116 208 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
paddy@41 209 if err != nil {
paddy@41 210 t.Fatalf("Error listing endpoints in %T: %s", store, err)
paddy@41 211 }
paddy@41 212 if len(endpoints) != 1 {
paddy@41 213 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
paddy@41 214 }
paddy@41 215 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0])
paddy@41 216 if !success {
paddy@41 217 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@41 218 }
paddy@116 219 err = context.RemoveEndpoint(client.ID, endpoint2.ID)
paddy@41 220 if err != nil {
paddy@41 221 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
paddy@41 222 }
paddy@116 223 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
paddy@41 224 if err != nil {
paddy@41 225 t.Fatalf("Error listing endpoints in %T: %s", store, err)
paddy@41 226 }
paddy@41 227 if len(endpoints) != 0 {
paddy@41 228 t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store)
paddy@31 229 }
paddy@31 230 }
paddy@31 231 }
paddy@39 232
paddy@39 233 func TestClientUpdates(t *testing.T) {
paddy@39 234 t.Parallel()
paddy@41 235 variations := 1 << 5
paddy@39 236 client := Client{
paddy@41 237 ID: uuid.NewID(),
paddy@41 238 Secret: "secret",
paddy@41 239 OwnerID: uuid.NewID(),
paddy@41 240 Name: "name",
paddy@41 241 Logo: "logo",
paddy@41 242 Website: "website",
paddy@39 243 }
paddy@39 244 for i := 0; i < variations; i++ {
paddy@41 245 var secret, name, logo, website string
paddy@39 246 change := ClientChange{}
paddy@151 247 client.ID = uuid.NewID()
paddy@39 248 expectation := client
paddy@39 249 result := client
paddy@39 250 if i&clientChangeSecret != 0 {
paddy@39 251 secret = fmt.Sprintf("secret-%d", i)
paddy@39 252 change.Secret = &secret
paddy@39 253 expectation.Secret = secret
paddy@39 254 }
paddy@39 255 if i&clientChangeOwnerID != 0 {
paddy@39 256 change.OwnerID = uuid.NewID()
paddy@39 257 expectation.OwnerID = change.OwnerID
paddy@39 258 }
paddy@39 259 if i&clientChangeName != 0 {
paddy@39 260 name = fmt.Sprintf("name-%d", i)
paddy@39 261 change.Name = &name
paddy@39 262 expectation.Name = name
paddy@39 263 }
paddy@39 264 if i&clientChangeLogo != 0 {
paddy@39 265 logo = fmt.Sprintf("logo-%d", i)
paddy@39 266 change.Logo = &logo
paddy@39 267 expectation.Logo = logo
paddy@39 268 }
paddy@39 269 if i&clientChangeWebsite != 0 {
paddy@39 270 website = fmt.Sprintf("website-%d", i)
paddy@39 271 change.Website = &website
paddy@39 272 expectation.Website = website
paddy@39 273 }
paddy@39 274 result.ApplyChange(change)
paddy@39 275 match, field, expected, got := compareClients(expectation, result)
paddy@39 276 if !match {
paddy@41 277 t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
paddy@39 278 }
paddy@39 279 for _, store := range clientStores {
paddy@116 280 context := Context{clients: store}
paddy@116 281 err := context.SaveClient(client)
paddy@39 282 if err != nil {
paddy@41 283 t.Fatalf("Error saving client in %T: %s", store, err)
paddy@39 284 }
paddy@116 285 err = context.UpdateClient(client.ID, change)
paddy@39 286 if err != nil {
paddy@41 287 t.Fatalf("Error updating client in %T: %s", store, err)
paddy@39 288 }
paddy@116 289 retrieved, err := context.GetClient(client.ID)
paddy@39 290 if err != nil {
paddy@116 291 t.Fatalf("Error getting client from %T: %s", store, err)
paddy@39 292 }
paddy@39 293 match, field, expected, got = compareClients(expectation, retrieved)
paddy@39 294 if !match {
paddy@41 295 t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
paddy@39 296 }
paddy@151 297 deleted := true
paddy@151 298 err = context.UpdateClient(client.ID, ClientChange{Deleted: &deleted})
paddy@39 299 if err != nil {
paddy@41 300 t.Fatalf("Error deleting client from %T: %s", store, err)
paddy@39 301 }
paddy@39 302 }
paddy@39 303 }
paddy@39 304 }
paddy@41 305
paddy@41 306 func TestClientEndpointChecks(t *testing.T) {
paddy@41 307 t.Parallel()
paddy@41 308 client := Client{
paddy@41 309 ID: uuid.NewID(),
paddy@41 310 Secret: "secret",
paddy@41 311 OwnerID: uuid.NewID(),
paddy@41 312 Name: "name",
paddy@41 313 Logo: "logo",
paddy@41 314 Website: "website",
paddy@41 315 }
paddy@41 316 endpoint1 := Endpoint{
paddy@41 317 ID: uuid.NewID(),
paddy@41 318 ClientID: client.ID,
paddy@149 319 Added: time.Now().Round(time.Millisecond),
paddy@116 320 URI: "https://www.example.com/first",
paddy@41 321 }
paddy@41 322 endpoint2 := Endpoint{
paddy@41 323 ID: uuid.NewID(),
paddy@41 324 ClientID: client.ID,
paddy@149 325 Added: time.Now().Round(time.Millisecond),
paddy@116 326 URI: "https://www.example.com/my/full/path",
paddy@41 327 }
paddy@41 328 candidates := map[string]bool{
paddy@41 329 "https://www.example.com/": false,
paddy@41 330 "https://www.example.com/first": true,
paddy@58 331 "https://www.example.com/first/extra/path": false,
paddy@41 332 "https://www.example.com/my": false,
paddy@41 333 "https://www.example.com/my/full/path": true,
paddy@41 334 }
paddy@41 335 for _, store := range clientStores {
paddy@116 336 context := Context{clients: store}
paddy@116 337 err := context.SaveClient(client)
paddy@41 338 if err != nil {
paddy@41 339 t.Fatalf("Error saving client in %T: %s", store, err)
paddy@41 340 }
paddy@151 341 err = context.AddEndpoints([]Endpoint{endpoint1})
paddy@41 342 if err != nil {
paddy@41 343 t.Fatalf("Error saving endpoint in %T: %s", store, err)
paddy@41 344 }
paddy@151 345 err = context.AddEndpoints([]Endpoint{endpoint2})
paddy@41 346 if err != nil {
paddy@41 347 t.Fatalf("Error saving endpoint in %T: %s", store, err)
paddy@41 348 }
paddy@41 349 for candidate, expectation := range candidates {
paddy@116 350 result, err := context.CheckEndpoint(client.ID, candidate)
paddy@54 351 if err != nil {
paddy@54 352 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
paddy@54 353 }
paddy@54 354 if result != expectation {
paddy@54 355 expectStr := "no"
paddy@54 356 resultStr := "a"
paddy@54 357 if expectation {
paddy@54 358 expectStr = "a"
paddy@54 359 resultStr = "no"
paddy@54 360 }
paddy@54 361 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
paddy@54 362 }
paddy@54 363 }
paddy@54 364 }
paddy@54 365 }
paddy@54 366
paddy@54 367 func TestClientEndpointChecksStrict(t *testing.T) {
paddy@54 368 t.Parallel()
paddy@54 369 client := Client{
paddy@54 370 ID: uuid.NewID(),
paddy@54 371 Secret: "secret",
paddy@54 372 OwnerID: uuid.NewID(),
paddy@54 373 Name: "name",
paddy@54 374 Logo: "logo",
paddy@54 375 Website: "website",
paddy@54 376 }
paddy@54 377 endpoint1 := Endpoint{
paddy@54 378 ID: uuid.NewID(),
paddy@54 379 ClientID: client.ID,
paddy@149 380 Added: time.Now().Round(time.Millisecond),
paddy@116 381 URI: "https://www.example.com/first",
paddy@54 382 }
paddy@54 383 endpoint2 := Endpoint{
paddy@54 384 ID: uuid.NewID(),
paddy@54 385 ClientID: client.ID,
paddy@149 386 Added: time.Now().Round(time.Millisecond),
paddy@116 387 URI: "https://www.example.com/my/full/path",
paddy@54 388 }
paddy@54 389 candidates := map[string]bool{
paddy@54 390 "https://www.example.com/": false,
paddy@54 391 "https://www.example.com/first": true,
paddy@54 392 "https://www.example.com/first/extra/path": false,
paddy@54 393 "https://www.example.com/my": false,
paddy@54 394 "https://www.example.com/my/full/path": true,
paddy@54 395 }
paddy@54 396 for _, store := range clientStores {
paddy@116 397 context := Context{clients: store}
paddy@116 398 err := context.SaveClient(client)
paddy@54 399 if err != nil {
paddy@54 400 t.Fatalf("Error saving client in %T: %s", store, err)
paddy@54 401 }
paddy@151 402 err = context.AddEndpoints([]Endpoint{endpoint1})
paddy@54 403 if err != nil {
paddy@54 404 t.Fatalf("Error saving endpoint in %T: %s", store, err)
paddy@54 405 }
paddy@151 406 err = context.AddEndpoints([]Endpoint{endpoint2})
paddy@54 407 if err != nil {
paddy@54 408 t.Fatalf("Error saving endpoint in %T: %s", store, err)
paddy@54 409 }
paddy@54 410 for candidate, expectation := range candidates {
paddy@116 411 result, err := context.CheckEndpoint(client.ID, candidate)
paddy@41 412 if err != nil {
paddy@41 413 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
paddy@41 414 }
paddy@41 415 if result != expectation {
paddy@41 416 expectStr := "no"
paddy@41 417 resultStr := "a"
paddy@41 418 if expectation {
paddy@41 419 expectStr = "a"
paddy@41 420 resultStr = "no"
paddy@41 421 }
paddy@41 422 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
paddy@41 423 }
paddy@41 424 }
paddy@41 425 }
paddy@41 426 }
paddy@43 427
paddy@43 428 func TestClientChangeValidation(t *testing.T) {
paddy@43 429 t.Parallel()
paddy@43 430 change := ClientChange{}
paddy@133 431 if err := change.Validate(); err[0] != ErrEmptyChange {
paddy@43 432 t.Errorf("Expected %s to give an error of %s, gave %s", "empty change", ErrEmptyChange, err)
paddy@43 433 }
paddy@133 434 names := map[string][]error{
paddy@133 435 "a": []error{ErrClientNameTooShort},
paddy@133 436 "ab": []error{},
paddy@133 437 "abc": []error{},
paddy@133 438 "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": []error{ErrClientNameTooLong},
paddy@43 439 }
paddy@43 440 for name, expectation := range names {
paddy@43 441 change = ClientChange{Name: &name}
paddy@133 442 errs := change.Validate()
paddy@133 443 if len(errs) != len(expectation) {
paddy@133 444 t.Errorf("Expected %s to give %d errors, gave %d", name, len(expectation), len(errs))
paddy@133 445 t.Logf("%+v", errs)
paddy@133 446 }
paddy@133 447 for pos, err := range errs {
paddy@133 448 if err != expectation[pos] {
paddy@133 449 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", name, expectation[pos], pos, err)
paddy@133 450 }
paddy@43 451 }
paddy@43 452 }
paddy@43 453 longPath := ""
paddy@43 454 for i := 0; i < 1025; i++ {
paddy@43 455 longPath = fmt.Sprintf("%s%d", longPath, i)
paddy@43 456 }
paddy@133 457 logos := map[string][]error{
paddy@133 458 "https://www.example.com/" + longPath: []error{ErrClientLogoTooLong},
paddy@133 459 "https://www.example.com/ab": []error{},
paddy@133 460 "www.example.com/ab": []error{ErrClientLogoNotURL},
paddy@133 461 "test": []error{ErrClientLogoNotURL},
paddy@133 462 "": []error{},
paddy@43 463 }
paddy@43 464 for logo, expectation := range logos {
paddy@43 465 change = ClientChange{Logo: &logo}
paddy@133 466 errs := change.Validate()
paddy@133 467 if len(errs) != len(expectation) {
paddy@133 468 t.Errorf("Expected %s to give %d errors, gave %d", logo, len(expectation), len(errs))
paddy@133 469 }
paddy@133 470 for pos, err := range errs {
paddy@133 471 if err != expectation[pos] {
paddy@133 472 t.Errorf("Expected %s to give an error of %s in positiong %d, gave %s", logo, expectation[pos], pos, err)
paddy@133 473 }
paddy@43 474 }
paddy@43 475 }
paddy@133 476 websites := map[string][]error{
paddy@133 477 "https://www.example.com/" + longPath: []error{ErrClientWebsiteTooLong},
paddy@133 478 "https://www.example.com/ab": []error{},
paddy@133 479 "www.example.com/ab": []error{ErrClientWebsiteNotURL},
paddy@133 480 "test": []error{ErrClientWebsiteNotURL},
paddy@133 481 "": []error{},
paddy@43 482 }
paddy@43 483 for website, expectation := range websites {
paddy@43 484 change = ClientChange{Website: &website}
paddy@133 485 errs := change.Validate()
paddy@133 486 if len(errs) != len(expectation) {
paddy@133 487 t.Errorf("Expected %s to give %d errors, gave %d", website, len(expectation), len(errs))
paddy@133 488 }
paddy@133 489 for pos, err := range errs {
paddy@133 490 if err != expectation[pos] {
paddy@133 491 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", website, expectation[pos], pos, err)
paddy@133 492 }
paddy@43 493 }
paddy@43 494 }
paddy@43 495 }
paddy@113 496
paddy@129 497 func TestGetClientAuth(t *testing.T) {
paddy@129 498 t.Parallel()
paddy@129 499 type clientAuthRequest struct {
paddy@129 500 username string
paddy@129 501 pass string
paddy@129 502 clientID string
paddy@129 503 allowPublic bool
paddy@129 504 expectedClientID uuid.ID
paddy@129 505 expectedClientSecret string
paddy@129 506 expectedValid bool
paddy@129 507 expectedCode int
paddy@129 508 expectedBody string
paddy@129 509 expectAuthenticateHeader bool
paddy@129 510 }
paddy@129 511 id := uuid.NewID()
paddy@129 512 tests := []clientAuthRequest{
paddy@129 513 {"", "", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 514 {"", "", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 515 {"", "no clientID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 516 {"", "no clientID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 517 {"not an actual id", "invalid client ID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 518 {"not an actual id", "invalid client ID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 519 {"", "", "not an actual id", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 520 {id.String(), "secret", "", true, id, "secret", true, http.StatusOK, "", false},
paddy@129 521 {id.String(), "secret", "", false, id, "secret", true, http.StatusOK, "", false},
paddy@129 522 {"", "", id.String(), true, id, "", true, http.StatusOK, "", false},
paddy@129 523 {"", "", id.String(), false, nil, "", false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
paddy@129 524 }
paddy@129 525 for pos, test := range tests {
paddy@129 526 t.Logf("Running test #%d, with request %+v", pos, test)
paddy@129 527 w := httptest.NewRecorder()
paddy@129 528 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@129 529 if err != nil {
paddy@129 530 t.Fatal("Can't build request:", err)
paddy@129 531 }
paddy@129 532 if test.username != "" || test.pass != "" {
paddy@129 533 r.SetBasicAuth(test.username, test.pass)
paddy@129 534 }
paddy@129 535 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@129 536 params := url.Values{}
paddy@129 537 params.Set("client_id", test.clientID)
paddy@129 538 body := bytes.NewBufferString(params.Encode())
paddy@129 539 r.Body = ioutil.NopCloser(body)
paddy@129 540 respID, respSecret, success := getClientAuth(w, r, test.allowPublic)
paddy@129 541 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
paddy@129 542 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
paddy@129 543 }
paddy@129 544 if test.expectedClientSecret != respSecret {
paddy@129 545 t.Errorf("Expected response secret to be '%s', got '%s'", test.expectedClientSecret, respSecret)
paddy@129 546 }
paddy@129 547 if test.expectedValid != success {
paddy@129 548 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
paddy@129 549 }
paddy@129 550 if test.expectedCode != w.Code {
paddy@129 551 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
paddy@129 552 }
paddy@129 553 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@129 554 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
paddy@129 555 }
paddy@129 556 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
paddy@129 557 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
paddy@129 558 }
paddy@129 559 }
paddy@129 560 }
paddy@129 561
paddy@113 562 func TestVerifyClient(t *testing.T) {
paddy@113 563 t.Parallel()
paddy@129 564 type verifyClientRequest struct {
paddy@129 565 username string
paddy@129 566 pass string
paddy@129 567 clientID string
paddy@129 568 allowPublic bool
paddy@129 569 expectedClientID uuid.ID
paddy@129 570 expectedValid bool
paddy@129 571 expectedCode int
paddy@129 572 expectedBody string
paddy@129 573 expectAuthenticateHeader bool
paddy@129 574 }
paddy@113 575 memstore := NewMemstore()
paddy@113 576 context := Context{
paddy@113 577 clients: memstore,
paddy@113 578 }
paddy@113 579 client := Client{
paddy@113 580 ID: uuid.NewID(),
paddy@113 581 Secret: "super secret!",
paddy@113 582 OwnerID: uuid.NewID(),
paddy@113 583 Name: "My test client",
paddy@113 584 Logo: "https://secondbit.org/logo.png",
paddy@113 585 Website: "https://secondbit.org/",
paddy@113 586 Type: "confidential",
paddy@113 587 }
paddy@113 588 err := context.SaveClient(client)
paddy@113 589 if err != nil {
paddy@113 590 t.Fatal("Could not save client:", err)
paddy@113 591 }
paddy@113 592 publicClient := Client{
paddy@113 593 ID: uuid.NewID(),
paddy@113 594 Secret: "",
paddy@113 595 OwnerID: uuid.NewID(),
paddy@113 596 Name: "A public client",
paddy@113 597 Logo: "https://secondbit.org/logo.png",
paddy@113 598 Website: "https://secondbit.org/",
paddy@113 599 Type: "public",
paddy@113 600 }
paddy@113 601 err = context.SaveClient(publicClient)
paddy@113 602 if err != nil {
paddy@113 603 t.Fatal("Could not save client:", err)
paddy@113 604 }
paddy@129 605 id := uuid.NewID()
paddy@129 606 tests := []verifyClientRequest{
paddy@129 607 {"", "", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 608 {"", "", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 609 {"", "no clientID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 610 {"", "no clientID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 611 {"not an actual id", "invalid client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 612 {"not an actual id", "invalid client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 613 {id.String(), "unsaved client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 614 {id.String(), "unsaved client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 615 {client.ID.String(), "wrong secret", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 616 {client.ID.String(), "wrong secret", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 617 {"", "", "not an actual id", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 618 {"", "", id.String(), true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 619 {client.ID.String(), client.Secret, "", true, client.ID, true, http.StatusOK, "", false},
paddy@129 620 {client.ID.String(), client.Secret, "", false, client.ID, true, http.StatusOK, "", false},
paddy@129 621 {"", "", publicClient.ID.String(), true, publicClient.ID, true, http.StatusOK, "", false},
paddy@129 622 {"", "", publicClient.ID.String(), false, nil, false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
paddy@113 623 }
paddy@113 624
paddy@129 625 for pos, test := range tests {
paddy@129 626 t.Logf("Running test #%d, with request %+v", pos, test)
paddy@129 627 w := httptest.NewRecorder()
paddy@129 628 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@129 629 if err != nil {
paddy@129 630 t.Fatal("Can't build request:", err)
paddy@129 631 }
paddy@129 632 if test.username != "" || test.pass != "" {
paddy@129 633 r.SetBasicAuth(test.username, test.pass)
paddy@129 634 }
paddy@129 635 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@129 636 params := url.Values{}
paddy@129 637 params.Set("client_id", test.clientID)
paddy@129 638 body := bytes.NewBufferString(params.Encode())
paddy@129 639 r.Body = ioutil.NopCloser(body)
paddy@129 640 respID, success := verifyClient(w, r, test.allowPublic, context)
paddy@129 641 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
paddy@129 642 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
paddy@129 643 }
paddy@129 644 if test.expectedValid != success {
paddy@129 645 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
paddy@129 646 }
paddy@129 647 if test.expectedCode != w.Code {
paddy@129 648 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
paddy@129 649 }
paddy@129 650 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@129 651 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
paddy@129 652 }
paddy@129 653 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
paddy@129 654 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
paddy@129 655 }
paddy@113 656 }
paddy@113 657 }
paddy@116 658
paddy@116 659 func TestCreateClientHandler(t *testing.T) {
paddy@116 660 t.Parallel()
paddy@116 661 memstore := NewMemstore()
paddy@116 662 c := Context{
paddy@116 663 clients: memstore,
paddy@116 664 profiles: memstore,
paddy@116 665 }
paddy@116 666 w := httptest.NewRecorder()
paddy@116 667 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/clients", nil)
paddy@116 668 if err != nil {
paddy@116 669 t.Fatal("Can't build request:", err)
paddy@116 670 }
paddy@116 671 r.Header.Set("Content-Type", "application/json")
paddy@116 672 CreateClientHandler(w, r, c)
paddy@116 673 if w.Code != http.StatusUnauthorized {
paddy@116 674 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 675 }
paddy@116 676 expected := `{"errors":[{"error":"access_denied"}]}`
paddy@116 677 result := strings.TrimSpace(w.Body.String())
paddy@116 678 if result != expected {
paddy@116 679 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 680 }
paddy@116 681 w = httptest.NewRecorder()
paddy@116 682 r.Header.Set("Authorization", "Not basic at all...")
paddy@116 683 CreateClientHandler(w, r, c)
paddy@116 684 if w.Code != http.StatusUnauthorized {
paddy@116 685 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 686 }
paddy@116 687 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 688 result = strings.TrimSpace(w.Body.String())
paddy@116 689 if result != expected {
paddy@116 690 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 691 }
paddy@116 692 w = httptest.NewRecorder()
paddy@116 693 r.Header.Set("Authorization", "Basic TotallyNotBase64Encoded")
paddy@116 694 CreateClientHandler(w, r, c)
paddy@116 695 if w.Code != http.StatusUnauthorized {
paddy@116 696 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 697 }
paddy@116 698 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 699 result = strings.TrimSpace(w.Body.String())
paddy@116 700 if result != expected {
paddy@116 701 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 702 }
paddy@116 703 w = httptest.NewRecorder()
paddy@116 704 r.Header.Set("Authorization", "Basic dGhpc2hhc25vY29sb24=")
paddy@116 705 CreateClientHandler(w, r, c)
paddy@116 706 if w.Code != http.StatusUnauthorized {
paddy@116 707 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 708 }
paddy@116 709 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 710 result = strings.TrimSpace(w.Body.String())
paddy@116 711 if result != expected {
paddy@116 712 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 713 }
paddy@116 714 profile := Profile{
paddy@116 715 ID: uuid.NewID(),
paddy@116 716 Name: "Test User",
paddy@116 717 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
paddy@116 718 Iterations: 1,
paddy@116 719 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
paddy@116 720 PassphraseScheme: 1,
paddy@116 721 Compromised: false,
paddy@116 722 LockedUntil: time.Time{},
paddy@116 723 PassphraseReset: "",
paddy@116 724 PassphraseResetCreated: time.Time{},
paddy@149 725 Created: time.Now().Round(time.Millisecond),
paddy@116 726 LastSeen: time.Time{},
paddy@116 727 }
paddy@116 728 login := Login{
paddy@116 729 Type: "email",
paddy@116 730 Value: "test@example.com",
paddy@116 731 ProfileID: profile.ID,
paddy@149 732 Created: time.Now().Round(time.Millisecond),
paddy@116 733 LastUsed: time.Time{},
paddy@116 734 }
paddy@116 735 w = httptest.NewRecorder()
paddy@116 736 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
paddy@116 737 CreateClientHandler(w, r, c)
paddy@116 738 if w.Code != http.StatusUnauthorized {
paddy@116 739 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 740 }
paddy@116 741 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 742 result = strings.TrimSpace(w.Body.String())
paddy@116 743 if result != expected {
paddy@116 744 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 745 }
paddy@116 746 err = c.SaveProfile(profile)
paddy@116 747 if err != nil {
paddy@116 748 t.Error("Error saving profile:", err)
paddy@116 749 }
paddy@116 750 err = c.AddLogin(login)
paddy@116 751 if err != nil {
paddy@116 752 t.Error("Error adding login:", err)
paddy@116 753 }
paddy@116 754 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
paddy@116 755 type testStruct struct {
paddy@116 756 request string
paddy@116 757 code int
paddy@116 758 resp response
paddy@116 759 }
paddy@116 760 tests := []testStruct{
paddy@116 761 {``, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/"}}}},
paddy@116 762 {`{}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
paddy@116 763 {`{"type":"notarealtype"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
paddy@116 764 {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrOverflow, Field: "/name"}}}},
paddy@116 765 {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrInsufficient, Field: "/name"}}}},
paddy@116 766 {`{"type":"public"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/name"}}}},
paddy@116 767 {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrOverflow, Field: "/name"}}}},
paddy@116 768 {`{"type":"public","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInsufficient, Field: "/name"}}}},
paddy@116 769 {`{"name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}}}},
paddy@116 770 {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}}}},
paddy@116 771 {`{"type":"public","name":"My Client"}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}},
paddy@116 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"}}}},
paddy@116 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"}}}},
paddy@116 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"}}}},
paddy@126 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"}}}},
paddy@116 776 }
paddy@116 777 for pos, test := range tests {
paddy@116 778 t.Logf("Test #%d: `%s`", pos, test.request)
paddy@116 779 w = httptest.NewRecorder()
paddy@116 780 body := bytes.NewBufferString(test.request)
paddy@116 781 r.Body = ioutil.NopCloser(body)
paddy@116 782 CreateClientHandler(w, r, c)
paddy@116 783 if w.Code != test.code {
paddy@116 784 t.Errorf("Expected response code to be %d, got %d", test.code, w.Code)
paddy@116 785 }
paddy@116 786 t.Logf("Response: %s", w.Body.String())
paddy@116 787 var res response
paddy@116 788 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@116 789 if err != nil {
paddy@116 790 t.Error("Unexpected error unmarshalling response:", err)
paddy@116 791 }
paddy@126 792 if len(res.Clients) > 0 {
paddy@126 793 if res.Clients[0].Type == "confidential" && res.Clients[0].Secret == "" {
paddy@126 794 t.Log("Client:", res.Clients[0])
paddy@126 795 t.Error("Expected confidential client to have a secret, but does not.")
paddy@126 796 } else if res.Clients[0].Type == "public" && res.Clients[0].Secret != "" {
paddy@126 797 t.Log("Client:", res.Clients[0])
paddy@126 798 t.Error("Expected public client to not have a secret, but it does.")
paddy@126 799 }
paddy@126 800 }
paddy@116 801 fillInServerGenerated(test.resp, res)
paddy@116 802 success, field, expectation, result := compareResponses(test.resp, res)
paddy@116 803 if !success {
paddy@116 804 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
paddy@116 805 }
paddy@116 806 }
paddy@116 807 }
paddy@128 808
paddy@139 809 func TestGetClientHandler(t *testing.T) {
paddy@139 810 t.Parallel()
paddy@139 811 memstore := NewMemstore()
paddy@139 812 c := Context{
paddy@139 813 clients: memstore,
paddy@139 814 profiles: memstore,
paddy@139 815 }
paddy@139 816 client := Client{
paddy@139 817 ID: uuid.NewID(),
paddy@139 818 Secret: "myawesomesecret",
paddy@139 819 OwnerID: uuid.NewID(),
paddy@139 820 Name: "Test Client",
paddy@139 821 Logo: "https://auth.secondbit.org/logo.png",
paddy@139 822 Website: "https://code.secondbit.org",
paddy@139 823 Type: clientTypeConfidential,
paddy@139 824 }
paddy@139 825 err := c.SaveClient(client)
paddy@139 826 if err != nil {
paddy@139 827 t.Fatal("Can't store client in memstore:", err)
paddy@139 828 }
paddy@139 829 profile := Profile{
paddy@139 830 ID: uuid.NewID(),
paddy@139 831 Name: "Test User",
paddy@139 832 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
paddy@139 833 Iterations: 1,
paddy@139 834 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
paddy@139 835 PassphraseScheme: 1,
paddy@139 836 Compromised: false,
paddy@139 837 LockedUntil: time.Time{},
paddy@139 838 PassphraseReset: "",
paddy@139 839 PassphraseResetCreated: time.Time{},
paddy@149 840 Created: time.Now().Round(time.Millisecond),
paddy@139 841 LastSeen: time.Time{},
paddy@139 842 }
paddy@139 843 login := Login{
paddy@139 844 Type: "email",
paddy@139 845 Value: "test@example.com",
paddy@139 846 ProfileID: profile.ID,
paddy@149 847 Created: time.Now().Round(time.Millisecond),
paddy@139 848 LastUsed: time.Time{},
paddy@139 849 }
paddy@139 850 err = c.SaveProfile(profile)
paddy@139 851 if err != nil {
paddy@139 852 t.Error("Error saving profile:", err)
paddy@139 853 }
paddy@139 854 err = c.AddLogin(login)
paddy@139 855 if err != nil {
paddy@139 856 t.Error("Error adding login:", err)
paddy@139 857 }
paddy@139 858 router := mux.NewRouter()
paddy@139 859 RegisterClientHandlers(router, c)
paddy@139 860 w := httptest.NewRecorder()
paddy@139 861 u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
paddy@139 862 r, err := http.NewRequest("GET", u, nil)
paddy@139 863 if err != nil {
paddy@139 864 t.Fatal("Can't build request:", err)
paddy@139 865 }
paddy@139 866 r.Header.Set("Content-Type", "application/json")
paddy@139 867 router.ServeHTTP(w, r)
paddy@139 868 if w.Code != http.StatusOK {
paddy@139 869 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
paddy@139 870 }
paddy@139 871 t.Logf("Response: %s", w.Body.String())
paddy@139 872 var res response
paddy@139 873 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@139 874 if err != nil {
paddy@139 875 t.Error("Unexpected error unmarshalling response:", err)
paddy@139 876 }
paddy@139 877 if len(res.Clients) != 1 {
paddy@139 878 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
paddy@139 879 }
paddy@139 880 if res.Clients[0].Secret != "" {
paddy@139 881 t.Error("Expected secret not to be set, but was set to", res.Clients[0].Secret)
paddy@139 882 }
paddy@139 883 // fill in the secret, which was omitted in the response
paddy@139 884 res.Clients[0].Secret = client.Secret
paddy@139 885 success, field, expectation, result := compareClients(client, res.Clients[0])
paddy@139 886 if !success {
paddy@139 887 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
paddy@139 888 }
paddy@139 889
paddy@139 890 // test for improperly formatted ID
paddy@139 891 u = "https://test.auth.secondbit.org/clients/notanID"
paddy@139 892 w = httptest.NewRecorder()
paddy@139 893 r, err = http.NewRequest("GET", u, nil)
paddy@139 894 if err != nil {
paddy@139 895 t.Fatal("Can't build request:", err)
paddy@139 896 }
paddy@139 897 r.Header.Set("Content-Type", "application/json")
paddy@139 898 router.ServeHTTP(w, r)
paddy@139 899 if w.Code != http.StatusBadRequest {
paddy@139 900 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
paddy@139 901 }
paddy@139 902 t.Logf("Response: %s", w.Body.String())
paddy@139 903 res = response{}
paddy@139 904 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@139 905 if err != nil {
paddy@139 906 t.Error("Unexpected error unmarshalling response:", err)
paddy@139 907 }
paddy@139 908 if len(res.Errors) != 1 {
paddy@139 909 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
paddy@139 910 }
paddy@139 911 e := requestError{Slug: requestErrInvalidFormat, Param: "id"}
paddy@139 912 success, field, expectation, result = compareErrors(e, res.Errors[0])
paddy@139 913 if !success {
paddy@139 914 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
paddy@139 915 }
paddy@139 916
paddy@139 917 // test for a non-existent client
paddy@139 918 u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
paddy@139 919 w = httptest.NewRecorder()
paddy@139 920 r, err = http.NewRequest("GET", u, nil)
paddy@139 921 if err != nil {
paddy@139 922 t.Fatal("Can't build request:", err)
paddy@139 923 }
paddy@139 924 r.Header.Set("Content-Type", "application/json")
paddy@139 925 router.ServeHTTP(w, r)
paddy@139 926 if w.Code != http.StatusNotFound {
paddy@139 927 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
paddy@139 928 }
paddy@139 929 t.Logf("Response: %s", w.Body.String())
paddy@139 930 res = response{}
paddy@139 931 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@139 932 if err != nil {
paddy@139 933 t.Error("Unexpected error unmarshalling response:", err)
paddy@139 934 }
paddy@139 935 if len(res.Errors) != 1 {
paddy@139 936 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
paddy@139 937 }
paddy@139 938 e = requestError{Slug: requestErrNotFound, Param: "id"}
paddy@139 939 success, field, expectation, result = compareErrors(e, res.Errors[0])
paddy@139 940 if !success {
paddy@139 941 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
paddy@139 942 }
paddy@139 943 }
paddy@139 944
paddy@139 945 func TestAuthenticatedGetClientHandler(t *testing.T) {
paddy@139 946 t.Parallel()
paddy@139 947 memstore := NewMemstore()
paddy@139 948 c := Context{
paddy@139 949 clients: memstore,
paddy@139 950 profiles: memstore,
paddy@139 951 }
paddy@139 952 client := Client{
paddy@139 953 ID: uuid.NewID(),
paddy@139 954 Secret: "myawesomesecret",
paddy@139 955 OwnerID: uuid.NewID(),
paddy@139 956 Name: "Test Client",
paddy@139 957 Logo: "https://auth.secondbit.org/logo.png",
paddy@139 958 Website: "https://code.secondbit.org",
paddy@139 959 Type: clientTypeConfidential,
paddy@139 960 }
paddy@139 961 err := c.SaveClient(client)
paddy@139 962 if err != nil {
paddy@139 963 t.Fatal("Can't store client in memstore:", err)
paddy@139 964 }
paddy@139 965 profile := Profile{
paddy@139 966 ID: client.OwnerID,
paddy@139 967 Name: "Test User",
paddy@139 968 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
paddy@139 969 Iterations: 1,
paddy@139 970 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
paddy@139 971 PassphraseScheme: 1,
paddy@139 972 Compromised: false,
paddy@139 973 LockedUntil: time.Time{},
paddy@139 974 PassphraseReset: "",
paddy@139 975 PassphraseResetCreated: time.Time{},
paddy@149 976 Created: time.Now().Round(time.Millisecond),
paddy@139 977 LastSeen: time.Time{},
paddy@139 978 }
paddy@139 979 login := Login{
paddy@139 980 Type: "email",
paddy@139 981 Value: "test@example.com",
paddy@139 982 ProfileID: profile.ID,
paddy@149 983 Created: time.Now().Round(time.Millisecond),
paddy@139 984 LastUsed: time.Time{},
paddy@139 985 }
paddy@139 986 err = c.SaveProfile(profile)
paddy@139 987 if err != nil {
paddy@139 988 t.Error("Error saving profile:", err)
paddy@139 989 }
paddy@139 990 err = c.AddLogin(login)
paddy@139 991 if err != nil {
paddy@139 992 t.Error("Error adding login:", err)
paddy@139 993 }
paddy@139 994 profile2 := Profile{
paddy@139 995 ID: uuid.NewID(),
paddy@139 996 Name: "Test User",
paddy@139 997 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
paddy@139 998 Iterations: 1,
paddy@139 999 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
paddy@139 1000 PassphraseScheme: 1,
paddy@139 1001 Compromised: false,
paddy@139 1002 LockedUntil: time.Time{},
paddy@139 1003 PassphraseReset: "",
paddy@139 1004 PassphraseResetCreated: time.Time{},
paddy@149 1005 Created: time.Now().Round(time.Millisecond),
paddy@139 1006 LastSeen: time.Time{},
paddy@139 1007 }
paddy@139 1008 login2 := Login{
paddy@139 1009 Type: "email",
paddy@139 1010 Value: "test2@example.com",
paddy@139 1011 ProfileID: profile2.ID,
paddy@149 1012 Created: time.Now().Round(time.Millisecond),
paddy@139 1013 LastUsed: time.Time{},
paddy@139 1014 }
paddy@139 1015 err = c.SaveProfile(profile2)
paddy@139 1016 if err != nil {
paddy@139 1017 t.Error("Error saving profile:", err)
paddy@139 1018 }
paddy@139 1019 err = c.AddLogin(login2)
paddy@139 1020 if err != nil {
paddy@139 1021 t.Error("Error adding login:", err)
paddy@139 1022 }
paddy@139 1023 router := mux.NewRouter()
paddy@139 1024 RegisterClientHandlers(router, c)
paddy@139 1025 w := httptest.NewRecorder()
paddy@139 1026 u := "https://test.auth.secondbit.org/clients/" + client.ID.String()
paddy@139 1027 r, err := http.NewRequest("GET", u, nil)
paddy@139 1028 if err != nil {
paddy@139 1029 t.Fatal("Can't build request:", err)
paddy@139 1030 }
paddy@139 1031 r.Header.Set("Content-Type", "application/json")
paddy@139 1032 r.SetBasicAuth(login.Value, "mysecurepassphrase")
paddy@139 1033 router.ServeHTTP(w, r)
paddy@139 1034 if w.Code != http.StatusOK {
paddy@139 1035 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
paddy@139 1036 }
paddy@139 1037 t.Logf("Response: %s", w.Body.String())
paddy@139 1038 var res response
paddy@139 1039 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@139 1040 if err != nil {
paddy@139 1041 t.Error("Unexpected error unmarshalling response:", err)
paddy@139 1042 }
paddy@139 1043 if len(res.Clients) != 1 {
paddy@139 1044 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
paddy@139 1045 }
paddy@139 1046 success, field, expectation, result := compareClients(client, res.Clients[0])
paddy@139 1047 if !success {
paddy@139 1048 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
paddy@139 1049 }
paddy@139 1050
paddy@139 1051 // test for improperly formatted ID
paddy@139 1052 u = "https://test.auth.secondbit.org/clients/notanID"
paddy@139 1053 w = httptest.NewRecorder()
paddy@139 1054 r, err = http.NewRequest("GET", u, nil)
paddy@139 1055 if err != nil {
paddy@139 1056 t.Fatal("Can't build request:", err)
paddy@139 1057 }
paddy@139 1058 r.Header.Set("Content-Type", "application/json")
paddy@139 1059 r.SetBasicAuth(login.Value, "mysecurepassphrase")
paddy@139 1060 router.ServeHTTP(w, r)
paddy@139 1061 if w.Code != http.StatusBadRequest {
paddy@139 1062 t.Errorf("Expected response code to be %d, got %d", http.StatusBadRequest, w.Code)
paddy@139 1063 }
paddy@139 1064 t.Logf("Response: %s", w.Body.String())
paddy@139 1065 res = response{}
paddy@139 1066 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@139 1067 if err != nil {
paddy@139 1068 t.Error("Unexpected error unmarshalling response:", err)
paddy@139 1069 }
paddy@139 1070 if len(res.Errors) != 1 {
paddy@139 1071 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
paddy@139 1072 }
paddy@139 1073 e := requestError{Slug: requestErrInvalidFormat, Param: "id"}
paddy@139 1074 success, field, expectation, result = compareErrors(e, res.Errors[0])
paddy@139 1075 if !success {
paddy@139 1076 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
paddy@139 1077 }
paddy@139 1078
paddy@139 1079 // test for a non-existent client
paddy@139 1080 u = "https://test.auth.secondbit.org/clients/" + uuid.NewID().String()
paddy@139 1081 w = httptest.NewRecorder()
paddy@139 1082 r, err = http.NewRequest("GET", u, nil)
paddy@139 1083 if err != nil {
paddy@139 1084 t.Fatal("Can't build request:", err)
paddy@139 1085 }
paddy@139 1086 r.Header.Set("Content-Type", "application/json")
paddy@139 1087 r.SetBasicAuth(login.Value, "mysecurepassphrase")
paddy@139 1088 router.ServeHTTP(w, r)
paddy@139 1089 if w.Code != http.StatusNotFound {
paddy@139 1090 t.Errorf("Expected response code to be %d, got %d", http.StatusNotFound, w.Code)
paddy@139 1091 }
paddy@139 1092 t.Logf("Response: %s", w.Body.String())
paddy@139 1093 res = response{}
paddy@139 1094 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@139 1095 if err != nil {
paddy@139 1096 t.Error("Unexpected error unmarshalling response:", err)
paddy@139 1097 }
paddy@139 1098 if len(res.Errors) != 1 {
paddy@139 1099 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
paddy@139 1100 }
paddy@139 1101 e = requestError{Slug: requestErrNotFound, Param: "id"}
paddy@139 1102 success, field, expectation, result = compareErrors(e, res.Errors[0])
paddy@139 1103 if !success {
paddy@139 1104 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
paddy@139 1105 }
paddy@139 1106
paddy@139 1107 // test for a wrong password
paddy@139 1108 u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
paddy@139 1109 w = httptest.NewRecorder()
paddy@139 1110 r, err = http.NewRequest("GET", u, nil)
paddy@139 1111 if err != nil {
paddy@139 1112 t.Fatal("Can't build request:", err)
paddy@139 1113 }
paddy@139 1114 r.Header.Set("Content-Type", "application/json")
paddy@139 1115 r.SetBasicAuth(login.Value, "notmypassphrase")
paddy@139 1116 router.ServeHTTP(w, r)
paddy@139 1117 if w.Code != http.StatusUnauthorized {
paddy@139 1118 t.Errorf("Expected response code to be %d, got %d", http.StatusUnauthorized, w.Code)
paddy@139 1119 }
paddy@139 1120 t.Logf("Response: %s", w.Body.String())
paddy@139 1121 res = response{}
paddy@139 1122 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@139 1123 if err != nil {
paddy@139 1124 t.Error("Unexpected error unmarshalling response:", err)
paddy@139 1125 }
paddy@139 1126 if len(res.Errors) != 1 {
paddy@139 1127 t.Errorf("Expected %d results in response, got %d", 1, len(res.Errors))
paddy@139 1128 }
paddy@139 1129 e = requestError{Slug: requestErrAccessDenied}
paddy@139 1130 success, field, expectation, result = compareErrors(e, res.Errors[0])
paddy@139 1131 if !success {
paddy@139 1132 t.Errorf("Unexpected result for %s in error response: expected %v, got %v", field, expectation, result)
paddy@139 1133 }
paddy@139 1134
paddy@139 1135 // test for a wrong account
paddy@139 1136 u = "https://test.auth.secondbit.org/clients/" + client.ID.String()
paddy@139 1137 w = httptest.NewRecorder()
paddy@139 1138 r, err = http.NewRequest("GET", u, nil)
paddy@139 1139 if err != nil {
paddy@139 1140 t.Fatal("Can't build request:", err)
paddy@139 1141 }
paddy@139 1142 r.Header.Set("Content-Type", "application/json")
paddy@139 1143 r.SetBasicAuth(login2.Value, "mysecurepassphrase")
paddy@139 1144 router.ServeHTTP(w, r)
paddy@139 1145 if w.Code != http.StatusOK {
paddy@139 1146 t.Errorf("Expected response code to be %d, got %d", http.StatusOK, w.Code)
paddy@139 1147 }
paddy@139 1148 t.Logf("Response: %s", w.Body.String())
paddy@139 1149 res = response{}
paddy@139 1150 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@139 1151 if err != nil {
paddy@139 1152 t.Error("Unexpected error unmarshalling response:", err)
paddy@139 1153 }
paddy@139 1154 if len(res.Clients) != 1 {
paddy@139 1155 t.Errorf("Expected %d results in response, got %d", 1, len(res.Clients))
paddy@139 1156 }
paddy@139 1157 if res.Clients[0].Secret != "" {
paddy@139 1158 t.Errorf("Expected client secret to be empty, got %s", res.Clients[0].Secret)
paddy@139 1159 }
paddy@139 1160 // fill the client's secret for comparison
paddy@139 1161 res.Clients[0].Secret = client.Secret
paddy@139 1162 success, field, expectation, result = compareClients(client, res.Clients[0])
paddy@139 1163 if !success {
paddy@139 1164 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
paddy@139 1165 }
paddy@139 1166 }
paddy@139 1167
paddy@128 1168 // BUG(paddy): We need to test the clientCredentialsValidate function.
paddy@131 1169 // BUG(paddy): We need to test the ListClientsHandler.