auth

Paddy 2015-01-24 Parent:23c1a07c8a61 Child:f474ce964dcf

129:4f5d13d2f7c7 Go to Latest

auth/client_test.go

Test our getClientAuth helper, switch to table-based tests. Our getClientAuth helper was being tested implicitly when we tested our verifyClient helper, but let's test them separately. While we're at it, let's use table based tests instead of copy and paste. I noticed a lot of copy/paste errors while I was updating this, and the less test code we have and the easier we make it to test new edge cases, the better of we are.

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@43 425 if err := change.Validate(); err != ErrEmptyChange {
paddy@43 426 t.Errorf("Expected %s to give an error of %s, gave %s", "empty change", ErrEmptyChange, err)
paddy@43 427 }
paddy@43 428 names := map[string]error{
paddy@43 429 "a": ErrClientNameTooShort,
paddy@43 430 "ab": nil,
paddy@43 431 "abc": nil,
paddy@43 432 "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopq": ErrClientNameTooLong,
paddy@43 433 }
paddy@43 434 for name, expectation := range names {
paddy@43 435 change = ClientChange{Name: &name}
paddy@43 436 if err := change.Validate(); err != expectation {
paddy@43 437 t.Errorf("Expected %s to give an error of %s, gave %s", name, expectation, err)
paddy@43 438 }
paddy@43 439 }
paddy@43 440 longPath := ""
paddy@43 441 for i := 0; i < 1025; i++ {
paddy@43 442 longPath = fmt.Sprintf("%s%d", longPath, i)
paddy@43 443 }
paddy@43 444 logos := map[string]error{
paddy@43 445 "https://www.example.com/" + longPath: ErrClientLogoTooLong,
paddy@43 446 "https://www.example.com/ab": nil,
paddy@43 447 "www.example.com/ab": ErrClientLogoNotURL,
paddy@43 448 "test": ErrClientLogoNotURL,
paddy@43 449 "": nil,
paddy@43 450 }
paddy@43 451 for logo, expectation := range logos {
paddy@43 452 change = ClientChange{Logo: &logo}
paddy@43 453 if err := change.Validate(); err != expectation {
paddy@43 454 t.Errorf("Expected %s to give an error of %s, gave %s", logo, expectation, err)
paddy@43 455 }
paddy@43 456 }
paddy@43 457 websites := map[string]error{
paddy@43 458 "https://www.example.com/" + longPath: ErrClientWebsiteTooLong,
paddy@43 459 "https://www.example.com/ab": nil,
paddy@43 460 "www.example.com/ab": ErrClientWebsiteNotURL,
paddy@43 461 "test": ErrClientWebsiteNotURL,
paddy@43 462 "": nil,
paddy@43 463 }
paddy@43 464 for website, expectation := range websites {
paddy@43 465 change = ClientChange{Website: &website}
paddy@43 466 if err := change.Validate(); err != expectation {
paddy@43 467 t.Errorf("Expected %s to give an error of %s, gave %s", website, expectation, err)
paddy@43 468 }
paddy@43 469 }
paddy@43 470 }
paddy@113 471
paddy@129 472 func TestGetClientAuth(t *testing.T) {
paddy@129 473 t.Parallel()
paddy@129 474 type clientAuthRequest struct {
paddy@129 475 username string
paddy@129 476 pass string
paddy@129 477 clientID string
paddy@129 478 allowPublic bool
paddy@129 479 expectedClientID uuid.ID
paddy@129 480 expectedClientSecret string
paddy@129 481 expectedValid bool
paddy@129 482 expectedCode int
paddy@129 483 expectedBody string
paddy@129 484 expectAuthenticateHeader bool
paddy@129 485 }
paddy@129 486 id := uuid.NewID()
paddy@129 487 tests := []clientAuthRequest{
paddy@129 488 {"", "", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 489 {"", "", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 490 {"", "no clientID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 491 {"", "no clientID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 492 {"not an actual id", "invalid client ID set", "", false, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 493 {"not an actual id", "invalid client ID set", "", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 494 {"", "", "not an actual id", true, nil, "", false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 495 {id.String(), "secret", "", true, id, "secret", true, http.StatusOK, "", false},
paddy@129 496 {id.String(), "secret", "", false, id, "secret", true, http.StatusOK, "", false},
paddy@129 497 {"", "", id.String(), true, id, "", true, http.StatusOK, "", false},
paddy@129 498 {"", "", id.String(), false, nil, "", false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
paddy@129 499 }
paddy@129 500 for pos, test := range tests {
paddy@129 501 t.Logf("Running test #%d, with request %+v", pos, test)
paddy@129 502 w := httptest.NewRecorder()
paddy@129 503 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@129 504 if err != nil {
paddy@129 505 t.Fatal("Can't build request:", err)
paddy@129 506 }
paddy@129 507 if test.username != "" || test.pass != "" {
paddy@129 508 r.SetBasicAuth(test.username, test.pass)
paddy@129 509 }
paddy@129 510 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@129 511 params := url.Values{}
paddy@129 512 params.Set("client_id", test.clientID)
paddy@129 513 body := bytes.NewBufferString(params.Encode())
paddy@129 514 r.Body = ioutil.NopCloser(body)
paddy@129 515 respID, respSecret, success := getClientAuth(w, r, test.allowPublic)
paddy@129 516 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
paddy@129 517 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
paddy@129 518 }
paddy@129 519 if test.expectedClientSecret != respSecret {
paddy@129 520 t.Errorf("Expected response secret to be '%s', got '%s'", test.expectedClientSecret, respSecret)
paddy@129 521 }
paddy@129 522 if test.expectedValid != success {
paddy@129 523 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
paddy@129 524 }
paddy@129 525 if test.expectedCode != w.Code {
paddy@129 526 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
paddy@129 527 }
paddy@129 528 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@129 529 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
paddy@129 530 }
paddy@129 531 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
paddy@129 532 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
paddy@129 533 }
paddy@129 534 }
paddy@129 535 }
paddy@129 536
paddy@113 537 func TestVerifyClient(t *testing.T) {
paddy@113 538 t.Parallel()
paddy@129 539 type verifyClientRequest struct {
paddy@129 540 username string
paddy@129 541 pass string
paddy@129 542 clientID string
paddy@129 543 allowPublic bool
paddy@129 544 expectedClientID uuid.ID
paddy@129 545 expectedValid bool
paddy@129 546 expectedCode int
paddy@129 547 expectedBody string
paddy@129 548 expectAuthenticateHeader bool
paddy@129 549 }
paddy@113 550 memstore := NewMemstore()
paddy@113 551 context := Context{
paddy@113 552 clients: memstore,
paddy@113 553 }
paddy@113 554 client := Client{
paddy@113 555 ID: uuid.NewID(),
paddy@113 556 Secret: "super secret!",
paddy@113 557 OwnerID: uuid.NewID(),
paddy@113 558 Name: "My test client",
paddy@113 559 Logo: "https://secondbit.org/logo.png",
paddy@113 560 Website: "https://secondbit.org/",
paddy@113 561 Type: "confidential",
paddy@113 562 }
paddy@113 563 err := context.SaveClient(client)
paddy@113 564 if err != nil {
paddy@113 565 t.Fatal("Could not save client:", err)
paddy@113 566 }
paddy@113 567 publicClient := Client{
paddy@113 568 ID: uuid.NewID(),
paddy@113 569 Secret: "",
paddy@113 570 OwnerID: uuid.NewID(),
paddy@113 571 Name: "A public client",
paddy@113 572 Logo: "https://secondbit.org/logo.png",
paddy@113 573 Website: "https://secondbit.org/",
paddy@113 574 Type: "public",
paddy@113 575 }
paddy@113 576 err = context.SaveClient(publicClient)
paddy@113 577 if err != nil {
paddy@113 578 t.Fatal("Could not save client:", err)
paddy@113 579 }
paddy@129 580 id := uuid.NewID()
paddy@129 581 tests := []verifyClientRequest{
paddy@129 582 {"", "", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 583 {"", "", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 584 {"", "no clientID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 585 {"", "no clientID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 586 {"not an actual id", "invalid client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 587 {"not an actual id", "invalid client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 588 {id.String(), "unsaved client ID set", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 589 {id.String(), "unsaved client ID set", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 590 {client.ID.String(), "wrong secret", "", false, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 591 {client.ID.String(), "wrong secret", "", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, true},
paddy@129 592 {"", "", "not an actual id", true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 593 {"", "", id.String(), true, nil, false, http.StatusUnauthorized, `{"error":"invalid_client"}`, false},
paddy@129 594 {client.ID.String(), client.Secret, "", true, client.ID, true, http.StatusOK, "", false},
paddy@129 595 {client.ID.String(), client.Secret, "", false, client.ID, true, http.StatusOK, "", false},
paddy@129 596 {"", "", publicClient.ID.String(), true, publicClient.ID, true, http.StatusOK, "", false},
paddy@129 597 {"", "", publicClient.ID.String(), false, nil, false, http.StatusBadRequest, `{"error":"unauthorized_client"}`, false},
paddy@113 598 }
paddy@113 599
paddy@129 600 for pos, test := range tests {
paddy@129 601 t.Logf("Running test #%d, with request %+v", pos, test)
paddy@129 602 w := httptest.NewRecorder()
paddy@129 603 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/oauth2/grant", nil)
paddy@129 604 if err != nil {
paddy@129 605 t.Fatal("Can't build request:", err)
paddy@129 606 }
paddy@129 607 if test.username != "" || test.pass != "" {
paddy@129 608 r.SetBasicAuth(test.username, test.pass)
paddy@129 609 }
paddy@129 610 r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
paddy@129 611 params := url.Values{}
paddy@129 612 params.Set("client_id", test.clientID)
paddy@129 613 body := bytes.NewBufferString(params.Encode())
paddy@129 614 r.Body = ioutil.NopCloser(body)
paddy@129 615 respID, success := verifyClient(w, r, test.allowPublic, context)
paddy@129 616 if (respID == nil && test.expectedClientID != nil) || (respID != nil && test.expectedClientID == nil) || !respID.Equal(test.expectedClientID) {
paddy@129 617 t.Errorf("Expected response ID to be %v, got %v", test.expectedClientID, respID)
paddy@129 618 }
paddy@129 619 if test.expectedValid != success {
paddy@129 620 t.Errorf("Expected success result to be %v, got %v", test.expectedValid, success)
paddy@129 621 }
paddy@129 622 if test.expectedCode != w.Code {
paddy@129 623 t.Errorf("Expected response code to be %d, got %d", test.expectedCode, w.Code)
paddy@129 624 }
paddy@129 625 if test.expectedBody != strings.TrimSpace(w.Body.String()) {
paddy@129 626 t.Errorf("Expected body to be '%s', got '%s'", test.expectedBody, strings.TrimSpace(w.Body.String()))
paddy@129 627 }
paddy@129 628 if test.expectAuthenticateHeader && w.Header().Get("WWW-Authenticate") != "Basic" {
paddy@129 629 t.Errorf(`Expected header WWW-Authenticate to be set to "Basic", got "%s"`, w.Header().Get("WWW-Authenticate"))
paddy@129 630 }
paddy@113 631 }
paddy@113 632 }
paddy@116 633
paddy@116 634 func TestCreateClientHandler(t *testing.T) {
paddy@116 635 t.Parallel()
paddy@116 636 memstore := NewMemstore()
paddy@116 637 c := Context{
paddy@116 638 clients: memstore,
paddy@116 639 profiles: memstore,
paddy@116 640 }
paddy@116 641 w := httptest.NewRecorder()
paddy@116 642 r, err := http.NewRequest("POST", "https://test.auth.secondbit.org/clients", nil)
paddy@116 643 if err != nil {
paddy@116 644 t.Fatal("Can't build request:", err)
paddy@116 645 }
paddy@116 646 r.Header.Set("Content-Type", "application/json")
paddy@116 647 CreateClientHandler(w, r, c)
paddy@116 648 if w.Code != http.StatusUnauthorized {
paddy@116 649 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 650 }
paddy@116 651 expected := `{"errors":[{"error":"access_denied"}]}`
paddy@116 652 result := strings.TrimSpace(w.Body.String())
paddy@116 653 if result != expected {
paddy@116 654 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 655 }
paddy@116 656 w = httptest.NewRecorder()
paddy@116 657 r.Header.Set("Authorization", "Not basic at all...")
paddy@116 658 CreateClientHandler(w, r, c)
paddy@116 659 if w.Code != http.StatusUnauthorized {
paddy@116 660 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 661 }
paddy@116 662 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 663 result = strings.TrimSpace(w.Body.String())
paddy@116 664 if result != expected {
paddy@116 665 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 666 }
paddy@116 667 w = httptest.NewRecorder()
paddy@116 668 r.Header.Set("Authorization", "Basic TotallyNotBase64Encoded")
paddy@116 669 CreateClientHandler(w, r, c)
paddy@116 670 if w.Code != http.StatusUnauthorized {
paddy@116 671 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 672 }
paddy@116 673 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 674 result = strings.TrimSpace(w.Body.String())
paddy@116 675 if result != expected {
paddy@116 676 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 677 }
paddy@116 678 w = httptest.NewRecorder()
paddy@116 679 r.Header.Set("Authorization", "Basic dGhpc2hhc25vY29sb24=")
paddy@116 680 CreateClientHandler(w, r, c)
paddy@116 681 if w.Code != http.StatusUnauthorized {
paddy@116 682 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 683 }
paddy@116 684 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 685 result = strings.TrimSpace(w.Body.String())
paddy@116 686 if result != expected {
paddy@116 687 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 688 }
paddy@116 689 profile := Profile{
paddy@116 690 ID: uuid.NewID(),
paddy@116 691 Name: "Test User",
paddy@116 692 Passphrase: "f3a4ac4f1d657b2e6e776d24213e39406d50a87a52691a2a78891425af1271d0",
paddy@116 693 Iterations: 1,
paddy@116 694 Salt: "d82d92cfa8bfb5a08270ebbf39a3710d24b352b937fcc8959ebcb40384cc616b",
paddy@116 695 PassphraseScheme: 1,
paddy@116 696 Compromised: false,
paddy@116 697 LockedUntil: time.Time{},
paddy@116 698 PassphraseReset: "",
paddy@116 699 PassphraseResetCreated: time.Time{},
paddy@116 700 Created: time.Now(),
paddy@116 701 LastSeen: time.Time{},
paddy@116 702 }
paddy@116 703 login := Login{
paddy@116 704 Type: "email",
paddy@116 705 Value: "test@example.com",
paddy@116 706 ProfileID: profile.ID,
paddy@116 707 Created: time.Now(),
paddy@116 708 LastUsed: time.Time{},
paddy@116 709 }
paddy@116 710 w = httptest.NewRecorder()
paddy@116 711 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
paddy@116 712 CreateClientHandler(w, r, c)
paddy@116 713 if w.Code != http.StatusUnauthorized {
paddy@116 714 t.Errorf("Expected status of %d, got status %d", http.StatusUnauthorized, w.Code)
paddy@116 715 }
paddy@116 716 expected = `{"errors":[{"error":"access_denied"}]}`
paddy@116 717 result = strings.TrimSpace(w.Body.String())
paddy@116 718 if result != expected {
paddy@116 719 t.Errorf("Expected response to be `%s`, got `%s`", expected, result)
paddy@116 720 }
paddy@116 721 err = c.SaveProfile(profile)
paddy@116 722 if err != nil {
paddy@116 723 t.Error("Error saving profile:", err)
paddy@116 724 }
paddy@116 725 err = c.AddLogin(login)
paddy@116 726 if err != nil {
paddy@116 727 t.Error("Error adding login:", err)
paddy@116 728 }
paddy@116 729 r.SetBasicAuth("test@example.com", "mysecurepassphrase")
paddy@116 730 type testStruct struct {
paddy@116 731 request string
paddy@116 732 code int
paddy@116 733 resp response
paddy@116 734 }
paddy@116 735 tests := []testStruct{
paddy@116 736 {``, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidFormat, Field: "/"}}}},
paddy@116 737 {`{}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
paddy@116 738 {`{"type":"notarealtype"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrMissing, Field: "/name"}}}},
paddy@116 739 {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrOverflow, Field: "/name"}}}},
paddy@116 740 {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}, {Slug: requestErrInsufficient, Field: "/name"}}}},
paddy@116 741 {`{"type":"public"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/name"}}}},
paddy@116 742 {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrOverflow, Field: "/name"}}}},
paddy@116 743 {`{"type":"public","name":"a"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInsufficient, Field: "/name"}}}},
paddy@116 744 {`{"name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrMissing, Field: "/type"}}}},
paddy@116 745 {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, response{Errors: []requestError{{Slug: requestErrInvalidValue, Field: "/type"}}}},
paddy@116 746 {`{"type":"public","name":"My Client"}`, http.StatusCreated, response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}},
paddy@116 747 {`{"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 748 {`{"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 749 {`{"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 750 {`{"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 751 }
paddy@116 752 for pos, test := range tests {
paddy@116 753 t.Logf("Test #%d: `%s`", pos, test.request)
paddy@116 754 w = httptest.NewRecorder()
paddy@116 755 body := bytes.NewBufferString(test.request)
paddy@116 756 r.Body = ioutil.NopCloser(body)
paddy@116 757 CreateClientHandler(w, r, c)
paddy@116 758 if w.Code != test.code {
paddy@116 759 t.Errorf("Expected response code to be %d, got %d", test.code, w.Code)
paddy@116 760 }
paddy@116 761 t.Logf("Response: %s", w.Body.String())
paddy@116 762 var res response
paddy@116 763 err = json.Unmarshal(w.Body.Bytes(), &res)
paddy@116 764 if err != nil {
paddy@116 765 t.Error("Unexpected error unmarshalling response:", err)
paddy@116 766 }
paddy@126 767 if len(res.Clients) > 0 {
paddy@126 768 if res.Clients[0].Type == "confidential" && res.Clients[0].Secret == "" {
paddy@126 769 t.Log("Client:", res.Clients[0])
paddy@126 770 t.Error("Expected confidential client to have a secret, but does not.")
paddy@126 771 } else if res.Clients[0].Type == "public" && res.Clients[0].Secret != "" {
paddy@126 772 t.Log("Client:", res.Clients[0])
paddy@126 773 t.Error("Expected public client to not have a secret, but it does.")
paddy@126 774 }
paddy@126 775 }
paddy@116 776 fillInServerGenerated(test.resp, res)
paddy@116 777 success, field, expectation, result := compareResponses(test.resp, res)
paddy@116 778 if !success {
paddy@116 779 t.Errorf("Unexpected result for %s in response: expected %v, got %v", field, expectation, result)
paddy@116 780 }
paddy@116 781 }
paddy@116 782 }
paddy@128 783
paddy@128 784 // BUG(paddy): We need to test the clientCredentialsValidate function.