auth

Paddy 2015-01-29 Parent:f474ce964dcf Child:026adb0c7fc4

133:bc842183181d Go to Latest

auth/client_test.go

Add Client updating from the API. Add a handler to update Clients using the API. Add a helper that will decode a request for us based on its Content-Type header. Change the ClientChange.Validate function to return as many errors as possible, as opposed to just the first error it encounters. Update the ClientChange.Validate tests to take advantage of the new signature.

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@113 7 "io/ioutil"
paddy@113 8 "net/http"
paddy@113 9 "net/http/httptest"
paddy@41 10 "net/url"
paddy@82 11 "sort"
paddy@113 12 "strings"
paddy@31 13 "testing"
paddy@41 14 "time"
paddy@31 15
paddy@107 16 "code.secondbit.org/uuid.hg"
paddy@31 17 )
paddy@31 18
paddy@39 19 const (
paddy@39 20 clientChangeSecret = 1 << iota
paddy@39 21 clientChangeOwnerID
paddy@39 22 clientChangeName
paddy@39 23 clientChangeLogo
paddy@39 24 clientChangeWebsite
paddy@39 25 )
paddy@39 26
paddy@57 27 var clientStores = []clientStore{NewMemstore()}
paddy@31 28
paddy@33 29 func compareClients(client1, client2 Client) (success bool, field string, val1, val2 interface{}) {
paddy@33 30 if !client1.ID.Equal(client2.ID) {
paddy@33 31 return false, "ID", client1.ID, client2.ID
paddy@33 32 }
paddy@33 33 if client1.Secret != client2.Secret {
paddy@33 34 return false, "secret", client1.Secret, client2.Secret
paddy@33 35 }
paddy@33 36 if !client1.OwnerID.Equal(client2.OwnerID) {
paddy@33 37 return false, "owner ID", client1.OwnerID, client2.OwnerID
paddy@33 38 }
paddy@33 39 if client1.Name != client2.Name {
paddy@33 40 return false, "name", client1.Name, client2.Name
paddy@33 41 }
paddy@33 42 if client1.Logo != client2.Logo {
paddy@33 43 return false, "logo", client1.Logo, client2.Logo
paddy@33 44 }
paddy@33 45 if client1.Website != client2.Website {
paddy@33 46 return false, "website", client1.Website, client2.Website
paddy@33 47 }
paddy@41 48 if client1.Type != client2.Type {
paddy@41 49 return false, "type", client1.Type, client2.Type
paddy@41 50 }
paddy@41 51 return true, "", nil, nil
paddy@41 52 }
paddy@41 53
paddy@41 54 func compareEndpoints(endpoint1, endpoint2 Endpoint) (success bool, field string, val1, val2 interface{}) {
paddy@41 55 if !endpoint1.ID.Equal(endpoint2.ID) {
paddy@41 56 return false, "ID", endpoint1.ID, endpoint2.ID
paddy@41 57 }
paddy@41 58 if !endpoint1.ClientID.Equal(endpoint2.ClientID) {
paddy@41 59 return false, "OwnerID", endpoint1.ClientID, endpoint2.ClientID
paddy@41 60 }
paddy@41 61 if !endpoint1.Added.Equal(endpoint2.Added) {
paddy@41 62 return false, "Added", endpoint1.Added, endpoint2.Added
paddy@41 63 }
paddy@116 64 if endpoint1.URI != endpoint2.URI {
paddy@41 65 return false, "URI", endpoint1.URI, endpoint2.URI
paddy@41 66 }
paddy@33 67 return true, "", nil, nil
paddy@33 68 }
paddy@33 69
paddy@31 70 func TestClientStoreSuccess(t *testing.T) {
paddy@36 71 t.Parallel()
paddy@31 72 client := Client{
paddy@41 73 ID: uuid.NewID(),
paddy@41 74 Secret: "secret",
paddy@41 75 OwnerID: uuid.NewID(),
paddy@41 76 Name: "name",
paddy@41 77 Logo: "logo",
paddy@41 78 Website: "website",
paddy@31 79 }
paddy@31 80 for _, store := range clientStores {
paddy@116 81 context := Context{clients: store}
paddy@116 82 err := context.SaveClient(client)
paddy@31 83 if err != nil {
paddy@41 84 t.Fatalf("Error saving client to %T: %s", store, err)
paddy@31 85 }
paddy@116 86 err = context.SaveClient(client)
paddy@33 87 if err != ErrClientAlreadyExists {
paddy@41 88 t.Fatalf("Expected ErrClientAlreadyExists, got %v from %T", err, store)
paddy@33 89 }
paddy@116 90 retrieved, err := context.GetClient(client.ID)
paddy@31 91 if err != nil {
paddy@41 92 t.Fatalf("Error retrieving client from %T: %s", store, err)
paddy@31 93 }
paddy@33 94 success, field, expectation, result := compareClients(client, retrieved)
paddy@33 95 if !success {
paddy@41 96 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@33 97 }
paddy@116 98 clients, err := context.ListClientsByOwner(client.OwnerID, 25, 0)
paddy@31 99 if err != nil {
paddy@41 100 t.Fatalf("Error retrieving clients by owner from %T: %s", store, err)
paddy@31 101 }
paddy@31 102 if len(clients) != 1 {
paddy@41 103 t.Fatalf("Expected 1 client in response from %T, got %+v", store, clients)
paddy@31 104 }
paddy@33 105 success, field, expectation, result = compareClients(client, clients[0])
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 err = context.DeleteClient(client.ID)
paddy@31 110 if err != nil {
paddy@41 111 t.Fatalf("Error deleting client from %T: %s", store, err)
paddy@31 112 }
paddy@116 113 err = context.DeleteClient(client.ID)
paddy@33 114 if err != ErrClientNotFound {
paddy@41 115 t.Fatalf("Expected ErrClientNotFound, got %s from %T", err, store)
paddy@33 116 }
paddy@116 117 retrieved, err = context.GetClient(client.ID)
paddy@31 118 if err != ErrClientNotFound {
paddy@41 119 t.Fatalf("Expected ErrClientNotFound from %T, got %+v and %s", store, retrieved, err)
paddy@31 120 }
paddy@116 121 clients, err = context.ListClientsByOwner(client.OwnerID, 25, 0)
paddy@31 122 if err != nil {
paddy@41 123 t.Fatalf("Error listing clients by owner from %T: %s", store, err)
paddy@31 124 }
paddy@31 125 if len(clients) != 0 {
paddy@41 126 t.Fatalf("Expected 0 clients in response from %T, got %+v", store, clients)
paddy@41 127 }
paddy@41 128 }
paddy@41 129 }
paddy@41 130
paddy@41 131 func TestEndpointStoreSuccess(t *testing.T) {
paddy@41 132 t.Parallel()
paddy@41 133 client := Client{
paddy@41 134 ID: uuid.NewID(),
paddy@41 135 Secret: "secret",
paddy@41 136 OwnerID: uuid.NewID(),
paddy@41 137 Name: "name",
paddy@41 138 Logo: "logo",
paddy@41 139 Website: "website",
paddy@41 140 }
paddy@41 141 endpoint1 := Endpoint{
paddy@41 142 ID: uuid.NewID(),
paddy@41 143 ClientID: client.ID,
paddy@41 144 Added: time.Now(),
paddy@116 145 URI: "https://www.example.com/",
paddy@41 146 }
paddy@41 147 endpoint2 := Endpoint{
paddy@41 148 ID: uuid.NewID(),
paddy@41 149 ClientID: client.ID,
paddy@41 150 Added: time.Now(),
paddy@116 151 URI: "https://www.example.com/my/full/path",
paddy@41 152 }
paddy@41 153 for _, store := range clientStores {
paddy@116 154 context := Context{clients: store}
paddy@116 155 err := context.SaveClient(client)
paddy@41 156 if err != nil {
paddy@41 157 t.Fatalf("Error saving client to %T: %s", store, err)
paddy@41 158 }
paddy@116 159 err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
paddy@41 160 if err != nil {
paddy@41 161 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
paddy@41 162 }
paddy@116 163 endpoints, err := context.ListEndpoints(client.ID, 10, 0)
paddy@41 164 if err != nil {
paddy@41 165 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
paddy@41 166 }
paddy@41 167 if len(endpoints) != 1 {
paddy@41 168 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
paddy@41 169 }
paddy@41 170 success, field, expectation, result := compareEndpoints(endpoint1, endpoints[0])
paddy@41 171 if !success {
paddy@41 172 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@41 173 }
paddy@116 174 err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
paddy@41 175 if err != nil {
paddy@41 176 t.Fatalf("Error adding endpoint to client in %T: %s", store, err)
paddy@41 177 }
paddy@116 178 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
paddy@41 179 if err != nil {
paddy@41 180 t.Fatalf("Error retrieving endpoints from %T: %s", store, err)
paddy@41 181 }
paddy@41 182 if len(endpoints) != 2 {
paddy@41 183 t.Fatalf("Expected %d endpoints, got %+v from %T", 2, endpoints, store)
paddy@41 184 }
paddy@41 185 sortedEnd := sortedEndpoints(endpoints)
paddy@41 186 sort.Sort(sortedEnd)
paddy@41 187 endpoints = []Endpoint(sortedEnd)
paddy@41 188 success, field, expectation, result = compareEndpoints(endpoint1, endpoints[0])
paddy@41 189 if !success {
paddy@41 190 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@41 191 }
paddy@41 192 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[1])
paddy@41 193 if !success {
paddy@41 194 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@41 195 }
paddy@116 196 err = context.RemoveEndpoint(client.ID, endpoint1.ID)
paddy@41 197 if err != nil {
paddy@41 198 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
paddy@41 199 }
paddy@116 200 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
paddy@41 201 if err != nil {
paddy@41 202 t.Fatalf("Error listing endpoints in %T: %s", store, err)
paddy@41 203 }
paddy@41 204 if len(endpoints) != 1 {
paddy@41 205 t.Fatalf("Expected %d endpoints, got %+v from %T", 1, endpoints, store)
paddy@41 206 }
paddy@41 207 success, field, expectation, result = compareEndpoints(endpoint2, endpoints[0])
paddy@41 208 if !success {
paddy@41 209 t.Fatalf("Expected field %s to be %v, but %T returned %v", field, expectation, store, result)
paddy@41 210 }
paddy@116 211 err = context.RemoveEndpoint(client.ID, endpoint2.ID)
paddy@41 212 if err != nil {
paddy@41 213 t.Fatalf("Error removing endpoint from client in %T: %s", store, err)
paddy@41 214 }
paddy@116 215 endpoints, err = context.ListEndpoints(client.ID, 10, 0)
paddy@41 216 if err != nil {
paddy@41 217 t.Fatalf("Error listing endpoints in %T: %s", store, err)
paddy@41 218 }
paddy@41 219 if len(endpoints) != 0 {
paddy@41 220 t.Fatalf("Expected %d endpoints, got %+v from %T", 0, endpoints, store)
paddy@31 221 }
paddy@31 222 }
paddy@31 223 }
paddy@39 224
paddy@39 225 func TestClientUpdates(t *testing.T) {
paddy@39 226 t.Parallel()
paddy@41 227 variations := 1 << 5
paddy@39 228 client := Client{
paddy@41 229 ID: uuid.NewID(),
paddy@41 230 Secret: "secret",
paddy@41 231 OwnerID: uuid.NewID(),
paddy@41 232 Name: "name",
paddy@41 233 Logo: "logo",
paddy@41 234 Website: "website",
paddy@39 235 }
paddy@39 236 for i := 0; i < variations; i++ {
paddy@41 237 var secret, name, logo, website string
paddy@39 238 change := ClientChange{}
paddy@39 239 expectation := client
paddy@39 240 result := client
paddy@39 241 if i&clientChangeSecret != 0 {
paddy@39 242 secret = fmt.Sprintf("secret-%d", i)
paddy@39 243 change.Secret = &secret
paddy@39 244 expectation.Secret = secret
paddy@39 245 }
paddy@39 246 if i&clientChangeOwnerID != 0 {
paddy@39 247 change.OwnerID = uuid.NewID()
paddy@39 248 expectation.OwnerID = change.OwnerID
paddy@39 249 }
paddy@39 250 if i&clientChangeName != 0 {
paddy@39 251 name = fmt.Sprintf("name-%d", i)
paddy@39 252 change.Name = &name
paddy@39 253 expectation.Name = name
paddy@39 254 }
paddy@39 255 if i&clientChangeLogo != 0 {
paddy@39 256 logo = fmt.Sprintf("logo-%d", i)
paddy@39 257 change.Logo = &logo
paddy@39 258 expectation.Logo = logo
paddy@39 259 }
paddy@39 260 if i&clientChangeWebsite != 0 {
paddy@39 261 website = fmt.Sprintf("website-%d", i)
paddy@39 262 change.Website = &website
paddy@39 263 expectation.Website = website
paddy@39 264 }
paddy@39 265 result.ApplyChange(change)
paddy@39 266 match, field, expected, got := compareClients(expectation, result)
paddy@39 267 if !match {
paddy@41 268 t.Fatalf("Expected field `%s` to be `%v`, got `%v`", field, expected, got)
paddy@39 269 }
paddy@39 270 for _, store := range clientStores {
paddy@116 271 context := Context{clients: store}
paddy@116 272 err := context.SaveClient(client)
paddy@39 273 if err != nil {
paddy@41 274 t.Fatalf("Error saving client in %T: %s", store, err)
paddy@39 275 }
paddy@116 276 err = context.UpdateClient(client.ID, change)
paddy@39 277 if err != nil {
paddy@41 278 t.Fatalf("Error updating client in %T: %s", store, err)
paddy@39 279 }
paddy@116 280 retrieved, err := context.GetClient(client.ID)
paddy@39 281 if err != nil {
paddy@116 282 t.Fatalf("Error getting client from %T: %s", store, err)
paddy@39 283 }
paddy@39 284 match, field, expected, got = compareClients(expectation, retrieved)
paddy@39 285 if !match {
paddy@41 286 t.Fatalf("Expected field `%s` to be `%v`, got `%v` from %T", field, expected, got, store)
paddy@39 287 }
paddy@116 288 err = context.DeleteClient(client.ID)
paddy@39 289 if err != nil {
paddy@41 290 t.Fatalf("Error deleting client from %T: %s", store, err)
paddy@39 291 }
paddy@116 292 err = context.UpdateClient(client.ID, change)
paddy@39 293 if err != ErrClientNotFound {
paddy@41 294 t.Fatalf("Expected ErrClientNotFound, got %v from %T", err, store)
paddy@39 295 }
paddy@39 296 }
paddy@39 297 }
paddy@39 298 }
paddy@41 299
paddy@41 300 func TestClientEndpointChecks(t *testing.T) {
paddy@41 301 t.Parallel()
paddy@41 302 client := Client{
paddy@41 303 ID: uuid.NewID(),
paddy@41 304 Secret: "secret",
paddy@41 305 OwnerID: uuid.NewID(),
paddy@41 306 Name: "name",
paddy@41 307 Logo: "logo",
paddy@41 308 Website: "website",
paddy@41 309 }
paddy@41 310 endpoint1 := Endpoint{
paddy@41 311 ID: uuid.NewID(),
paddy@41 312 ClientID: client.ID,
paddy@41 313 Added: time.Now(),
paddy@116 314 URI: "https://www.example.com/first",
paddy@41 315 }
paddy@41 316 endpoint2 := Endpoint{
paddy@41 317 ID: uuid.NewID(),
paddy@41 318 ClientID: client.ID,
paddy@41 319 Added: time.Now(),
paddy@116 320 URI: "https://www.example.com/my/full/path",
paddy@41 321 }
paddy@41 322 candidates := map[string]bool{
paddy@41 323 "https://www.example.com/": false,
paddy@41 324 "https://www.example.com/first": true,
paddy@58 325 "https://www.example.com/first/extra/path": false,
paddy@41 326 "https://www.example.com/my": false,
paddy@41 327 "https://www.example.com/my/full/path": true,
paddy@41 328 }
paddy@41 329 for _, store := range clientStores {
paddy@116 330 context := Context{clients: store}
paddy@116 331 err := context.SaveClient(client)
paddy@41 332 if err != nil {
paddy@41 333 t.Fatalf("Error saving client in %T: %s", store, err)
paddy@41 334 }
paddy@116 335 err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
paddy@41 336 if err != nil {
paddy@41 337 t.Fatalf("Error saving endpoint in %T: %s", store, err)
paddy@41 338 }
paddy@116 339 err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
paddy@41 340 if err != nil {
paddy@41 341 t.Fatalf("Error saving endpoint in %T: %s", store, err)
paddy@41 342 }
paddy@41 343 for candidate, expectation := range candidates {
paddy@116 344 result, err := context.CheckEndpoint(client.ID, candidate)
paddy@54 345 if err != nil {
paddy@54 346 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
paddy@54 347 }
paddy@54 348 if result != expectation {
paddy@54 349 expectStr := "no"
paddy@54 350 resultStr := "a"
paddy@54 351 if expectation {
paddy@54 352 expectStr = "a"
paddy@54 353 resultStr = "no"
paddy@54 354 }
paddy@54 355 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
paddy@54 356 }
paddy@54 357 }
paddy@54 358 }
paddy@54 359 }
paddy@54 360
paddy@54 361 func TestClientEndpointChecksStrict(t *testing.T) {
paddy@54 362 t.Parallel()
paddy@54 363 client := Client{
paddy@54 364 ID: uuid.NewID(),
paddy@54 365 Secret: "secret",
paddy@54 366 OwnerID: uuid.NewID(),
paddy@54 367 Name: "name",
paddy@54 368 Logo: "logo",
paddy@54 369 Website: "website",
paddy@54 370 }
paddy@54 371 endpoint1 := Endpoint{
paddy@54 372 ID: uuid.NewID(),
paddy@54 373 ClientID: client.ID,
paddy@54 374 Added: time.Now(),
paddy@116 375 URI: "https://www.example.com/first",
paddy@54 376 }
paddy@54 377 endpoint2 := Endpoint{
paddy@54 378 ID: uuid.NewID(),
paddy@54 379 ClientID: client.ID,
paddy@54 380 Added: time.Now(),
paddy@116 381 URI: "https://www.example.com/my/full/path",
paddy@54 382 }
paddy@54 383 candidates := map[string]bool{
paddy@54 384 "https://www.example.com/": false,
paddy@54 385 "https://www.example.com/first": true,
paddy@54 386 "https://www.example.com/first/extra/path": false,
paddy@54 387 "https://www.example.com/my": false,
paddy@54 388 "https://www.example.com/my/full/path": true,
paddy@54 389 }
paddy@54 390 for _, store := range clientStores {
paddy@116 391 context := Context{clients: store}
paddy@116 392 err := context.SaveClient(client)
paddy@54 393 if err != nil {
paddy@54 394 t.Fatalf("Error saving client in %T: %s", store, err)
paddy@54 395 }
paddy@116 396 err = context.AddEndpoints(client.ID, []Endpoint{endpoint1})
paddy@54 397 if err != nil {
paddy@54 398 t.Fatalf("Error saving endpoint in %T: %s", store, err)
paddy@54 399 }
paddy@116 400 err = context.AddEndpoints(client.ID, []Endpoint{endpoint2})
paddy@54 401 if err != nil {
paddy@54 402 t.Fatalf("Error saving endpoint in %T: %s", store, err)
paddy@54 403 }
paddy@54 404 for candidate, expectation := range candidates {
paddy@116 405 result, err := context.CheckEndpoint(client.ID, candidate)
paddy@41 406 if err != nil {
paddy@41 407 t.Fatalf("Error checking endpoint %s in %T: %s", candidate, store, err)
paddy@41 408 }
paddy@41 409 if result != expectation {
paddy@41 410 expectStr := "no"
paddy@41 411 resultStr := "a"
paddy@41 412 if expectation {
paddy@41 413 expectStr = "a"
paddy@41 414 resultStr = "no"
paddy@41 415 }
paddy@41 416 t.Errorf("Expected %s match for %s in %T, got %s match", expectStr, candidate, store, resultStr)
paddy@41 417 }
paddy@41 418 }
paddy@41 419 }
paddy@41 420 }
paddy@43 421
paddy@43 422 func TestClientChangeValidation(t *testing.T) {
paddy@43 423 t.Parallel()
paddy@43 424 change := ClientChange{}
paddy@133 425 if err := change.Validate(); err[0] != ErrEmptyChange {
paddy@43 426 t.Errorf("Expected %s to give an error of %s, gave %s", "empty change", ErrEmptyChange, err)
paddy@43 427 }
paddy@133 428 names := map[string][]error{
paddy@133 429 "a": []error{ErrClientNameTooShort},
paddy@133 430 "ab": []error{},
paddy@133 431 "abc": []error{},
paddy@133 432 "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": []error{ErrClientNameTooLong},
paddy@43 433 }
paddy@43 434 for name, expectation := range names {
paddy@43 435 change = ClientChange{Name: &name}
paddy@133 436 errs := change.Validate()
paddy@133 437 if len(errs) != len(expectation) {
paddy@133 438 t.Errorf("Expected %s to give %d errors, gave %d", name, len(expectation), len(errs))
paddy@133 439 t.Logf("%+v", errs)
paddy@133 440 }
paddy@133 441 for pos, err := range errs {
paddy@133 442 if err != expectation[pos] {
paddy@133 443 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", name, expectation[pos], pos, err)
paddy@133 444 }
paddy@43 445 }
paddy@43 446 }
paddy@43 447 longPath := ""
paddy@43 448 for i := 0; i < 1025; i++ {
paddy@43 449 longPath = fmt.Sprintf("%s%d", longPath, i)
paddy@43 450 }
paddy@133 451 logos := map[string][]error{
paddy@133 452 "https://www.example.com/" + longPath: []error{ErrClientLogoTooLong},
paddy@133 453 "https://www.example.com/ab": []error{},
paddy@133 454 "www.example.com/ab": []error{ErrClientLogoNotURL},
paddy@133 455 "test": []error{ErrClientLogoNotURL},
paddy@133 456 "": []error{},
paddy@43 457 }
paddy@43 458 for logo, expectation := range logos {
paddy@43 459 change = ClientChange{Logo: &logo}
paddy@133 460 errs := change.Validate()
paddy@133 461 if len(errs) != len(expectation) {
paddy@133 462 t.Errorf("Expected %s to give %d errors, gave %d", logo, len(expectation), len(errs))
paddy@133 463 }
paddy@133 464 for pos, err := range errs {
paddy@133 465 if err != expectation[pos] {
paddy@133 466 t.Errorf("Expected %s to give an error of %s in positiong %d, gave %s", logo, expectation[pos], pos, err)
paddy@133 467 }
paddy@43 468 }
paddy@43 469 }
paddy@133 470 websites := map[string][]error{
paddy@133 471 "https://www.example.com/" + longPath: []error{ErrClientWebsiteTooLong},
paddy@133 472 "https://www.example.com/ab": []error{},
paddy@133 473 "www.example.com/ab": []error{ErrClientWebsiteNotURL},
paddy@133 474 "test": []error{ErrClientWebsiteNotURL},
paddy@133 475 "": []error{},
paddy@43 476 }
paddy@43 477 for website, expectation := range websites {
paddy@43 478 change = ClientChange{Website: &website}
paddy@133 479 errs := change.Validate()
paddy@133 480 if len(errs) != len(expectation) {
paddy@133 481 t.Errorf("Expected %s to give %d errors, gave %d", website, len(expectation), len(errs))
paddy@133 482 }
paddy@133 483 for pos, err := range errs {
paddy@133 484 if err != expectation[pos] {
paddy@133 485 t.Errorf("Expected %s to give an error of %s in position %d, gave %s", website, expectation[pos], pos, err)
paddy@133 486 }
paddy@43 487 }
paddy@43 488 }
paddy@43 489 }
paddy@113 490
paddy@129 491 func TestGetClientAuth(t *testing.T) {
paddy@129 492 t.Parallel()
paddy@129 493 type clientAuthRequest struct {
paddy@129 494 username string
paddy@129 495 pass string
paddy@129 496 clientID string
paddy@129 497 allowPublic bool
paddy@129 498 expectedClientID uuid.ID
paddy@129 499 expectedClientSecret string
paddy@129 500 expectedValid bool
paddy@129 501 expectedCode int
paddy@129 502 expectedBody string
paddy@129 503 expectAuthenticateHeader bool
paddy@129 504 }
paddy@129 505 id := uuid.NewID()
paddy@129 506 tests := []clientAuthRequest{
paddy@129 507 {"", "", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 508 {"", "", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 509 {"", "no clientID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 510 {"", "no clientID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 511 {"not an actual id", "invalid client ID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 512 {"not an actual id", "invalid client ID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 513 {"", "", "not an actual id", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 514 {id.String(), "secret", "", true, id, "secret", true, http.StatusOK, "", false},
paddy@129 515 {id.String(), "secret", "", false, id, "secret", true, http.StatusOK, "", false},
paddy@129 516 {"", "", id.String(), true, id, "", true, http.StatusOK, "", false},
paddy@129 517 {"", "", id.String(), false, nil, "", false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
paddy@129 518 }
paddy@129 519 for pos, test := range tests {
paddy@129 520 t.Logf("Running test #%d, with request %+v", pos, test)
paddy@129 521 w := httptest.NewRecorder()
paddy@129 522 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@129 523 if err != nil {
paddy@129 524 t.Fatal("Can't build request:", err)
paddy@129 525 }
paddy@129 526 if test.username != "" || test.pass != "" {
paddy@129 527 r.SetBasicAuth(test.username, test.pass)
paddy@129 528 }
paddy@129 529 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@129 530 params := url.Values{}
paddy@129 531 params.Set("client_id", test.clientID)
paddy@129 532 body := bytes.NewBufferString(params.Encode())
paddy@129 533 r.Body = ioutil.NopCloser(body)
paddy@129 534 respID, respSecret, success := getClientAuth(w, r, test.allowPublic)
paddy@129 535 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
paddy@129 536 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
paddy@129 537 }
paddy@129 538 if test.expectedClientSecret != respSecret {
paddy@129 539 t.Errorf("Expected response secret to be '%s', got '%s'", test.expectedClientSecret, respSecret)
paddy@129 540 }
paddy@129 541 if test.expectedValid != success {
paddy@129 542 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
paddy@129 543 }
paddy@129 544 if test.expectedCode != w.Code {
paddy@129 545 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
paddy@129 546 }
paddy@129 547 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@129 548 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
paddy@129 549 }
paddy@129 550 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
paddy@129 551 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
paddy@129 552 }
paddy@129 553 }
paddy@129 554 }
paddy@129 555
paddy@113 556 func TestVerifyClient(t *testing.T) {
paddy@113 557 t.Parallel()
paddy@129 558 type verifyClientRequest struct {
paddy@129 559 username string
paddy@129 560 pass string
paddy@129 561 clientID string
paddy@129 562 allowPublic bool
paddy@129 563 expectedClientID uuid.ID
paddy@129 564 expectedValid bool
paddy@129 565 expectedCode int
paddy@129 566 expectedBody string
paddy@129 567 expectAuthenticateHeader bool
paddy@129 568 }
paddy@113 569 memstore := NewMemstore()
paddy@113 570 context := Context{
paddy@113 571 clients: memstore,
paddy@113 572 }
paddy@113 573 client := Client{
paddy@113 574 ID: uuid.NewID(),
paddy@113 575 Secret: "super secret!",
paddy@113 576 OwnerID: uuid.NewID(),
paddy@113 577 Name: "My test client",
paddy@113 578 Logo: "https://secondbit.org/logo.png",
paddy@113 579 Website: "https://secondbit.org/",
paddy@113 580 Type: "confidential",
paddy@113 581 }
paddy@113 582 err := context.SaveClient(client)
paddy@113 583 if err != nil {
paddy@113 584 t.Fatal("Could not save client:", err)
paddy@113 585 }
paddy@113 586 publicClient := Client{
paddy@113 587 ID: uuid.NewID(),
paddy@113 588 Secret: "",
paddy@113 589 OwnerID: uuid.NewID(),
paddy@113 590 Name: "A public client",
paddy@113 591 Logo: "https://secondbit.org/logo.png",
paddy@113 592 Website: "https://secondbit.org/",
paddy@113 593 Type: "public",
paddy@113 594 }
paddy@113 595 err = context.SaveClient(publicClient)
paddy@113 596 if err != nil {
paddy@113 597 t.Fatal("Could not save client:", err)
paddy@113 598 }
paddy@129 599 id := uuid.NewID()
paddy@129 600 tests := []verifyClientRequest{
paddy@129 601 {"", "", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 602 {"", "", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 603 {"", "no clientID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 604 {"", "no clientID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 605 {"not an actual id", "invalid client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 606 {"not an actual id", "invalid client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 607 {id.String(), "unsaved client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 608 {id.String(), "unsaved client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 609 {client.ID.String(), "wrong secret", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 610 {client.ID.String(), "wrong secret", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 611 {"", "", "not an actual id", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 612 {"", "", id.String(), true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 613 {client.ID.String(), client.Secret, "", true, client.ID, true, http.StatusOK, "", false},
paddy@129 614 {client.ID.String(), client.Secret, "", false, client.ID, true, http.StatusOK, "", false},
paddy@129 615 {"", "", publicClient.ID.String(), true, publicClient.ID, true, http.StatusOK, "", false},
paddy@129 616 {"", "", publicClient.ID.String(), false, nil, false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
paddy@113 617 }
paddy@113 618
paddy@129 619 for pos, test := range tests {
paddy@129 620 t.Logf("Running test #%d, with request %+v", pos, test)
paddy@129 621 w := httptest.NewRecorder()
paddy@129 622 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@129 623 if err != nil {
paddy@129 624 t.Fatal("Can't build request:", err)
paddy@129 625 }
paddy@129 626 if test.username != "" || test.pass != "" {
paddy@129 627 r.SetBasicAuth(test.username, test.pass)
paddy@129 628 }
paddy@129 629 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@129 630 params := url.Values{}
paddy@129 631 params.Set("client_id", test.clientID)
paddy@129 632 body := bytes.NewBufferString(params.Encode())
paddy@129 633 r.Body = ioutil.NopCloser(body)
paddy@129 634 respID, success := verifyClient(w, r, test.allowPublic, context)
paddy@129 635 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
paddy@129 636 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
paddy@129 637 }
paddy@129 638 if test.expectedValid != success {
paddy@129 639 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
paddy@129 640 }
paddy@129 641 if test.expectedCode != w.Code {
paddy@129 642 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
paddy@129 643 }
paddy@129 644 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@129 645 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
paddy@129 646 }
paddy@129 647 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
paddy@129 648 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
paddy@129 649 }
paddy@113 650 }
paddy@113 651 }
paddy@116 652
paddy@116 653 func TestCreateClientHandler(t *testing.T) {
paddy@116 654 t.Parallel()
paddy@116 655 memstore := NewMemstore()
paddy@116 656 c := Context{
paddy@116 657 clients: memstore,
paddy@116 658 profiles: memstore,
paddy@116 659 }
paddy@116 660 w := httptest.NewRecorder()
paddy@116 661 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/clients", nil)
paddy@116 662 if err != nil {
paddy@116 663 t.Fatal("Can't build request:", err)
paddy@116 664 }
paddy@116 665 r.Header.Set("Content-Type", "application/json")
paddy@116 666 CreateClientHandler(w, r, c)
paddy@116 667 if w.Code != http.StatusUnauthorized {
paddy@116 668 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 669 }
paddy@116 670 expected := `{"errors":[{"error":"access_denied"}]}`
paddy@116 671 result := strings.TrimSpace(w.Body.String())
paddy@116 672 if result != expected {
paddy@116 673 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 674 }
paddy@116 675 w = httptest.NewRecorder()
paddy@116 676 r.Header.Set("Authorization", "Not basic at all...")
paddy@116 677 CreateClientHandler(w, r, c)
paddy@116 678 if w.Code != http.StatusUnauthorized {
paddy@116 679 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 680 }
paddy@116 681 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 682 result = strings.TrimSpace(w.Body.String())
paddy@116 683 if result != expected {
paddy@116 684 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 685 }
paddy@116 686 w = httptest.NewRecorder()
paddy@116 687 r.Header.Set("Authorization", "Basic TotallyNotBase64Encoded")
paddy@116 688 CreateClientHandler(w, r, c)
paddy@116 689 if w.Code != http.StatusUnauthorized {
paddy@116 690 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 691 }
paddy@116 692 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 693 result = strings.TrimSpace(w.Body.String())
paddy@116 694 if result != expected {
paddy@116 695 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 696 }
paddy@116 697 w = httptest.NewRecorder()
paddy@116 698 r.Header.Set("Authorization", "Basic dGhpc2hhc25vY29sb24=")
paddy@116 699 CreateClientHandler(w, r, c)
paddy@116 700 if w.Code != http.StatusUnauthorized {
paddy@116 701 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 702 }
paddy@116 703 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 704 result = strings.TrimSpace(w.Body.String())
paddy@116 705 if result != expected {
paddy@116 706 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 707 }
paddy@116 708 profile := Profile{
paddy@116 709 ID: uuid.NewID(),
paddy@116 710 Name: "Test User",
paddy@116 711 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
paddy@116 712 Iterations: 1,
paddy@116 713 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
paddy@116 714 PassphraseScheme: 1,
paddy@116 715 Compromised: false,
paddy@116 716 LockedUntil: time.Time{},
paddy@116 717 PassphraseReset: "",
paddy@116 718 PassphraseResetCreated: time.Time{},
paddy@116 719 Created: time.Now(),
paddy@116 720 LastSeen: time.Time{},
paddy@116 721 }
paddy@116 722 login := Login{
paddy@116 723 Type: "email",
paddy@116 724 Value: "test@example.com",
paddy@116 725 ProfileID: profile.ID,
paddy@116 726 Created: time.Now(),
paddy@116 727 LastUsed: time.Time{},
paddy@116 728 }
paddy@116 729 w = httptest.NewRecorder()
paddy@116 730 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
paddy@116 731 CreateClientHandler(w, r, c)
paddy@116 732 if w.Code != http.StatusUnauthorized {
paddy@116 733 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 734 }
paddy@116 735 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 736 result = strings.TrimSpace(w.Body.String())
paddy@116 737 if result != expected {
paddy@116 738 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 739 }
paddy@116 740 err = c.SaveProfile(profile)
paddy@116 741 if err != nil {
paddy@116 742 t.Error("Error saving profile:", err)
paddy@116 743 }
paddy@116 744 err = c.AddLogin(login)
paddy@116 745 if err != nil {
paddy@116 746 t.Error("Error adding login:", err)
paddy@116 747 }
paddy@116 748 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
paddy@116 749 type testStruct struct {
paddy@116 750 request string
paddy@116 751 code int
paddy@116 752 resp response
paddy@116 753 }
paddy@116 754 tests := []testStruct{
paddy@116 755 {``, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/"}}}},
paddy@116 756 {`{}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
paddy@116 757 {`{"type":"notarealtype"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
paddy@116 758 {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrOverflow, Field: "/name"}}}},
paddy@116 759 {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrInsufficient, Field: "/name"}}}},
paddy@116 760 {`{"type":"public"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/name"}}}},
paddy@116 761 {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrOverflow, Field: "/name"}}}},
paddy@116 762 {`{"type":"public","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInsufficient, Field: "/name"}}}},
paddy@116 763 {`{"name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}}}},
paddy@116 764 {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}}}},
paddy@116 765 {`{"type":"public","name":"My Client"}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}},
paddy@116 766 {`{"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 767 {`{"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 768 {`{"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 769 {`{"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 770 }
paddy@116 771 for pos, test := range tests {
paddy@116 772 t.Logf("Test #%d: `%s`", pos, test.request)
paddy@116 773 w = httptest.NewRecorder()
paddy@116 774 body := bytes.NewBufferString(test.request)
paddy@116 775 r.Body = ioutil.NopCloser(body)
paddy@116 776 CreateClientHandler(w, r, c)
paddy@116 777 if w.Code != test.code {
paddy@116 778 t.Errorf("Expected response code to be %d, got %d", test.code, w.Code)
paddy@116 779 }
paddy@116 780 t.Logf("Response: %s", w.Body.String())
paddy@116 781 var res response
paddy@116 782 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@116 783 if err != nil {
paddy@116 784 t.Error("Unexpected error unmarshalling response:", err)
paddy@116 785 }
paddy@126 786 if len(res.Clients) > 0 {
paddy@126 787 if res.Clients[0].Type == "confidential" && res.Clients[0].Secret == "" {
paddy@126 788 t.Log("Client:", res.Clients[0])
paddy@126 789 t.Error("Expected confidential client to have a secret, but does not.")
paddy@126 790 } else if res.Clients[0].Type == "public" && res.Clients[0].Secret != "" {
paddy@126 791 t.Log("Client:", res.Clients[0])
paddy@126 792 t.Error("Expected public client to not have a secret, but it does.")
paddy@126 793 }
paddy@126 794 }
paddy@116 795 fillInServerGenerated(test.resp, res)
paddy@116 796 success, field, expectation, result := compareResponses(test.resp, res)
paddy@116 797 if !success {
paddy@116 798 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
paddy@116 799 }
paddy@116 800 }
paddy@116 801 }
paddy@128 802
paddy@128 803 // BUG(paddy): We need to test the clientCredentialsValidate function.
paddy@131 804 // BUG(paddy): We need to test the GetClientHandler.
paddy@131 805 // BUG(paddy): We need to test the ListClientsHandler.