auth

Paddy 2015-04-07 Parent:3e8964a914ef Child:8ecb60d29b0d

155:762953f6a7f2 Go to Latest

auth/client_test.go

Implement postgres version of the tokenStore. Create a postgres implementation for the tokenStore. Note that because pq doesn't support Postgres' array types (see https://github.com/lib/pq/issues/49), we couldn't just store the token.Scopes field as a Postgres array of varchars, which would have been the ideal. Instead, we need a many-to-many table that maps tokens to scopes. This meant we needed a special tokenScope type for our database mapping, and we needed to complicate the token storage/retrieval functions a little bit. It's kind of ugly, I'm not a huge fan of it, and I'd much rather be using the Postgres array types, but... well, here we are. We also added the postgres tokenStore to our slice of tokenStores to test when the correct environment variables are present. We wrote initialization SQL for the tables required by the postgres tokenStore. Also, added a helper script for emptying the test database, because I got tired of doing it by hand. We should be doing that in an automated fashion in the tests themselves, but that would mean extending the *Store interfaces.

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