auth

Paddy 2015-06-29 Parent:8ecb60d29b0d

175:aa14e29b666f Go to Latest

auth/client_test.go

Create Docker image for authd. Create a Dockerfile for authd, which will wrap the compiled Go binary up into a tiny little Docker image. Create an authd/build-docker.sh script that will build the statically-linked binary in a Docker container, so the authd Docker image can use it. We had to include ca-certificates.crt in the Dockerfile, as well, so we could communicate over SSL with things. A wrapper.sh file is included that will pull the JWT_SECRET environment variable out of a kubernetes secrets file, which is a handy wrapper to have. Finally, we added the authd/docker-authd binary to the .hgignore.

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@172 759 resp Response
paddy@116 760 }
paddy@116 761 tests := []testStruct{
paddy@172 762 {``, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidFormat, Field: "/"}}}},
paddy@172 763 {`{}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrMissing, Field: "/type"}, {Slug: RequestErrMissing, Field: "/name"}}}},
paddy@172 764 {`{"type":"notarealtype"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}, {Slug: RequestErrMissing, Field: "/name"}}}},
paddy@172 765 {`{"type":"notarealtype","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}, {Slug: RequestErrOverflow, Field: "/name"}}}},
paddy@172 766 {`{"type":"notarealtype","name":"a"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}, {Slug: RequestErrInsufficient, Field: "/name"}}}},
paddy@172 767 {`{"type":"public"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrMissing, Field: "/name"}}}},
paddy@172 768 {`{"type":"public","name":"myreallylongnameislongerthatthemaximumnamelength"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrOverflow, Field: "/name"}}}},
paddy@172 769 {`{"type":"public","name":"a"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInsufficient, Field: "/name"}}}},
paddy@172 770 {`{"name":"My Client"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrMissing, Field: "/type"}}}},
paddy@172 771 {`{"type":"notarealtype","name":"My Client"}`, http.StatusBadRequest, Response{Errors: []RequestError{{Slug: RequestErrInvalidValue, Field: "/type"}}}},
paddy@172 772 {`{"type":"public","name":"My Client"}`, http.StatusCreated, Response{Clients: []Client{{Name: "My Client", OwnerID: profile.ID, Type: "public"}}}},
paddy@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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@172 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.