auth

Paddy 2015-03-21 Parent:026adb0c7fc4 Child:8267e1c8bcd1

148:06fb735031bb Go to Latest

auth/client_test.go

Do a first, naive pass at storing profiles in Postgres. This is untested against an actual database. It's a best-guess attempt at SQL. It _should_ work. I think. Start storing things in Postgres, starting with Profiles and Logins. This necessitates the addition of a Deleted property to the Profile type, because I'm not deleting those in case of accidental deletion. Logins, though, we'll delete. This also necessitates updating the profileStore interface to no longer have a deleteProfile method, because we're tracking that through updates now. Then we need to update our profileStore tests, because they no longer clean up after themselves. Which, come to think of it, may cause some problems later.

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