github.com/go-kivik/kivik/v4@v4.3.2/x/server/server_test.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 12 13 //go:build !js 14 15 package server 16 17 import ( 18 "context" 19 "encoding/base64" 20 "encoding/json" 21 "io" 22 "net/http" 23 "net/http/httptest" 24 "os" 25 "regexp" 26 "strings" 27 "testing" 28 "time" 29 30 "github.com/go-playground/validator/v10" 31 "gitlab.com/flimzy/testy" 32 33 "github.com/go-kivik/kivik/v4" 34 _ "github.com/go-kivik/kivik/v4/x/fsdb" // Filesystem driver 35 _ "github.com/go-kivik/kivik/v4/x/memorydb" // Memory driver 36 "github.com/go-kivik/kivik/v4/x/server/auth" 37 "github.com/go-kivik/kivik/v4/x/server/config" 38 ) 39 40 var v = validator.New(validator.WithRequiredStructEnabled()) 41 42 const ( 43 userAdmin = "admin" 44 userBob = "bob" 45 userAlice = "alice" 46 userCharlie = "charlie" 47 userDavid = "davic" 48 userErin = "erin" 49 userFrank = "frank" 50 userReplicator = "replicator" 51 userDBUpdates = "db_updates" 52 userDesign = "design" 53 testPassword = "abc123" 54 roleFoo = "foo" 55 roleBar = "bar" 56 roleBaz = "baz" 57 ) 58 59 func testUserStore(t *testing.T) *auth.MemoryUserStore { 60 t.Helper() 61 us := auth.NewMemoryUserStore() 62 if err := us.AddUser(userAdmin, testPassword, []string{auth.RoleAdmin}); err != nil { 63 t.Fatal(err) 64 } 65 if err := us.AddUser(userBob, testPassword, []string{auth.RoleReader}); err != nil { 66 t.Fatal(err) 67 } 68 if err := us.AddUser(userAlice, testPassword, []string{auth.RoleWriter}); err != nil { 69 t.Fatal(err) 70 } 71 if err := us.AddUser(userCharlie, testPassword, []string{auth.RoleWriter, roleFoo}); err != nil { 72 t.Fatal(err) 73 } 74 if err := us.AddUser(userDavid, testPassword, []string{auth.RoleWriter, roleBar}); err != nil { 75 t.Fatal(err) 76 } 77 if err := us.AddUser(userErin, testPassword, []string{auth.RoleWriter}); err != nil { 78 t.Fatal(err) 79 } 80 if err := us.AddUser(userFrank, testPassword, []string{auth.RoleWriter, roleBaz}); err != nil { 81 t.Fatal(err) 82 } 83 if err := us.AddUser(userReplicator, testPassword, []string{auth.RoleReplicator}); err != nil { 84 t.Fatal(err) 85 } 86 if err := us.AddUser(userDBUpdates, testPassword, []string{auth.RoleDBUpdates}); err != nil { 87 t.Fatal(err) 88 } 89 if err := us.AddUser(userDesign, testPassword, []string{auth.RoleDesign}); err != nil { 90 t.Fatal(err) 91 } 92 return us 93 } 94 95 func basicAuth(user string) string { 96 return "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+testPassword)) 97 } 98 99 type serverTest struct { 100 name string 101 client *kivik.Client 102 driver, dsn string 103 init func(t *testing.T, client *kivik.Client) 104 extraOptions []Option 105 method string 106 path string 107 headers map[string]string 108 authUser string 109 body io.Reader 110 wantStatus int 111 wantBodyRE string 112 wantJSON interface{} 113 check func(t *testing.T, client *kivik.Client) 114 115 // if target is specified, it is expected to be a struct into which the 116 // response body will be unmarshaled, then validated. 117 target interface{} 118 } 119 120 type serverTests []serverTest 121 122 func (s serverTests) Run(t *testing.T) { 123 t.Helper() 124 for _, tt := range s { 125 tt := tt 126 t.Run(tt.name, func(t *testing.T) { 127 t.Parallel() 128 driver, dsn := "fs", "testdata/fsdb" 129 if tt.dsn != "" { 130 dsn = tt.dsn 131 } 132 client := tt.client 133 if client == nil { 134 if tt.driver != "" { 135 driver = tt.driver 136 } 137 if driver == "fs" { 138 dsn = testy.CopyTempDir(t, dsn, 0) 139 t.Cleanup(func() { 140 _ = os.RemoveAll(dsn) 141 }) 142 } 143 var err error 144 client, err = kivik.New(driver, dsn) 145 if err != nil { 146 t.Fatal(err) 147 } 148 } 149 if tt.init != nil { 150 tt.init(t, client) 151 } 152 us := testUserStore(t) 153 const secret = "foo" 154 opts := append([]Option{ 155 WithUserStores(us), 156 WithAuthHandlers(auth.BasicAuth()), 157 WithAuthHandlers(auth.CookieAuth(secret, time.Hour)), 158 }, tt.extraOptions...) 159 160 s := New(client, opts...) 161 body := tt.body 162 if body == nil { 163 body = strings.NewReader("") 164 } 165 req, err := http.NewRequest(tt.method, tt.path, body) 166 if err != nil { 167 t.Fatal(err) 168 } 169 for k, v := range tt.headers { 170 req.Header.Set(k, v) 171 } 172 if tt.authUser != "" { 173 user, err := us.UserCtx(context.Background(), tt.authUser) 174 if err != nil { 175 t.Fatal(err) 176 } 177 req.AddCookie(&http.Cookie{ 178 Name: kivik.SessionCookieName, 179 Value: auth.CreateAuthToken(user.Name, user.Salt, secret, time.Now().Unix()), 180 }) 181 } 182 183 rec := httptest.NewRecorder() 184 s.ServeHTTP(rec, req) 185 186 res := rec.Result() 187 if res.StatusCode != tt.wantStatus { 188 t.Errorf("Unexpected response status: %d %s", res.StatusCode, http.StatusText(res.StatusCode)) 189 } 190 switch { 191 case tt.target != nil: 192 if err := json.NewDecoder(res.Body).Decode(tt.target); err != nil { 193 t.Fatal(err) 194 } 195 if err := v.Struct(tt.target); err != nil { 196 t.Fatalf("response does not match expectations: %s\n%v", err, tt.target) 197 } 198 case tt.wantBodyRE != "": 199 re := regexp.MustCompile(tt.wantBodyRE) 200 body, err := io.ReadAll(res.Body) 201 if err != nil { 202 t.Fatal(err) 203 } 204 if !re.Match(body) { 205 t.Errorf("Unexpected response body:\n%s", body) 206 } 207 default: 208 if d := testy.DiffAsJSON(tt.wantJSON, res.Body); d != nil { 209 t.Error(d) 210 } 211 } 212 if tt.check != nil { 213 tt.check(t, client) 214 } 215 }) 216 } 217 } 218 219 func TestServer(t *testing.T) { 220 t.Parallel() 221 222 tests := serverTests{ 223 { 224 name: "root", 225 method: http.MethodGet, 226 path: "/", 227 wantStatus: http.StatusOK, 228 wantJSON: map[string]interface{}{ 229 "couchdb": "Welcome", 230 "vendor": map[string]interface{}{ 231 "name": "Kivik", 232 "version": kivik.Version, 233 }, 234 "version": kivik.Version, 235 }, 236 }, 237 { 238 name: "active tasks", 239 method: http.MethodGet, 240 path: "/_active_tasks", 241 headers: map[string]string{"Authorization": basicAuth(userAdmin)}, 242 wantStatus: http.StatusOK, 243 wantJSON: []interface{}{}, 244 }, 245 { 246 name: "all dbs", 247 method: http.MethodGet, 248 path: "/_all_dbs", 249 headers: map[string]string{"Authorization": basicAuth(userAdmin)}, 250 wantStatus: http.StatusOK, 251 wantJSON: []string{"bobsdb", "db1", "db2"}, 252 }, 253 { 254 name: "all dbs, cookie auth", 255 method: http.MethodGet, 256 path: "/_all_dbs", 257 authUser: userAdmin, 258 wantStatus: http.StatusOK, 259 wantJSON: []string{"bobsdb", "db1", "db2"}, 260 }, 261 { 262 name: "all dbs, non-admin", 263 method: http.MethodGet, 264 path: "/_all_dbs", 265 headers: map[string]string{"Authorization": basicAuth(userBob)}, 266 wantStatus: http.StatusForbidden, 267 wantJSON: map[string]interface{}{ 268 "error": "forbidden", 269 "reason": "Admin privileges required", 270 }, 271 }, 272 { 273 name: "all dbs, descending", 274 method: http.MethodGet, 275 path: "/_all_dbs?descending=true", 276 headers: map[string]string{"Authorization": basicAuth(userAdmin)}, 277 wantStatus: http.StatusOK, 278 wantJSON: []string{"db2", "db1", "bobsdb"}, 279 }, 280 { 281 name: "db info", 282 method: http.MethodGet, 283 path: "/db2", 284 headers: map[string]string{"Authorization": basicAuth(userAdmin)}, 285 wantStatus: http.StatusOK, 286 wantJSON: map[string]interface{}{ 287 "db_name": "db2", 288 "compact_running": false, 289 "data_size": 0, 290 "disk_size": 0, 291 "doc_count": 0, 292 "doc_del_count": 0, 293 "update_seq": "", 294 }, 295 }, 296 { 297 name: "db info HEAD", 298 method: http.MethodHead, 299 path: "/db2", 300 headers: map[string]string{"Authorization": basicAuth(userAdmin)}, 301 wantStatus: http.StatusOK, 302 }, 303 { 304 name: "start session, no content type header", 305 method: http.MethodPost, 306 path: "/_session", 307 body: strings.NewReader(`name=root&password=abc123`), 308 wantStatus: http.StatusUnsupportedMediaType, 309 wantJSON: map[string]interface{}{ 310 "error": "bad_content_type", 311 "reason": "Content-Type must be 'application/x-www-form-urlencoded' or 'application/json'", 312 }, 313 }, 314 { 315 name: "start session, invalid content type", 316 method: http.MethodPost, 317 path: "/_session", 318 body: strings.NewReader(`name=root&password=abc123`), 319 headers: map[string]string{"Content-Type": "application/xml"}, 320 wantStatus: http.StatusUnsupportedMediaType, 321 wantJSON: map[string]interface{}{ 322 "error": "bad_content_type", 323 "reason": "Content-Type must be 'application/x-www-form-urlencoded' or 'application/json'", 324 }, 325 }, 326 { 327 name: "start session, no user name", 328 method: http.MethodPost, 329 path: "/_session", 330 body: strings.NewReader(`{}`), 331 headers: map[string]string{"Content-Type": "application/json"}, 332 wantStatus: http.StatusBadRequest, 333 wantJSON: map[string]interface{}{ 334 "error": "bad_request", 335 "reason": "request body must contain a username", 336 }, 337 }, 338 { 339 name: "start session, success", 340 method: http.MethodPost, 341 path: "/_session", 342 body: strings.NewReader(`{"name":"admin","password":"abc123"}`), 343 headers: map[string]string{"Content-Type": "application/json"}, 344 wantStatus: http.StatusOK, 345 wantJSON: map[string]interface{}{ 346 "ok": true, 347 "name": userAdmin, 348 "roles": []string{"_admin"}, 349 }, 350 }, 351 { 352 name: "delete session", 353 method: http.MethodDelete, 354 path: "/_session", 355 authUser: userAdmin, 356 wantStatus: http.StatusOK, 357 wantJSON: map[string]interface{}{ 358 "ok": true, 359 }, 360 }, 361 { 362 name: "_up", 363 method: http.MethodGet, 364 path: "/_up", 365 wantStatus: http.StatusOK, 366 wantJSON: map[string]interface{}{ 367 "status": "ok", 368 }, 369 }, 370 { 371 name: "all config", 372 method: http.MethodGet, 373 path: "/_node/_local/_config", 374 authUser: userAdmin, 375 wantStatus: http.StatusOK, 376 wantJSON: map[string]interface{}{ 377 "couchdb": map[string]interface{}{ 378 "users_db_suffix": "_users", 379 }, 380 }, 381 }, 382 { 383 name: "all config, non-admin", 384 method: http.MethodGet, 385 path: "/_node/_local/_config", 386 authUser: userBob, 387 wantStatus: http.StatusForbidden, 388 wantJSON: map[string]interface{}{ 389 "error": "forbidden", 390 "reason": "Admin privileges required", 391 }, 392 }, 393 { 394 name: "all config, no such node", 395 method: http.MethodGet, 396 path: "/_node/asdf/_config", 397 authUser: userAdmin, 398 wantStatus: http.StatusNotFound, 399 wantJSON: map[string]interface{}{ 400 "error": "not_found", 401 "reason": "no such node: asdf", 402 }, 403 }, 404 { 405 name: "config section", 406 method: http.MethodGet, 407 path: "/_node/_local/_config/couchdb", 408 authUser: userAdmin, 409 wantStatus: http.StatusOK, 410 wantJSON: map[string]interface{}{ 411 "users_db_suffix": "_users", 412 }, 413 }, 414 { 415 name: "config key", 416 method: http.MethodGet, 417 path: "/_node/_local/_config/couchdb/users_db_suffix", 418 authUser: userAdmin, 419 wantStatus: http.StatusOK, 420 wantJSON: "_users", 421 }, 422 { 423 name: "reload config", 424 method: http.MethodPost, 425 path: "/_node/_local/_config/_reload", 426 authUser: userAdmin, 427 wantStatus: http.StatusOK, 428 wantJSON: map[string]bool{"ok": true}, 429 }, 430 { 431 name: "set new config key", 432 method: http.MethodPut, 433 path: "/_node/_local/_config/foo/bar", 434 body: strings.NewReader(`"oink"`), 435 authUser: userAdmin, 436 wantStatus: http.StatusOK, 437 wantJSON: "", 438 }, 439 { 440 name: "set existing config key", 441 method: http.MethodPut, 442 path: "/_node/_local/_config/couchdb/users_db_suffix", 443 body: strings.NewReader(`"oink"`), 444 authUser: userAdmin, 445 wantStatus: http.StatusOK, 446 wantJSON: "_users", 447 }, 448 { 449 name: "delete existing config key", 450 method: http.MethodDelete, 451 path: "/_node/_local/_config/couchdb/users_db_suffix", 452 authUser: userAdmin, 453 wantStatus: http.StatusOK, 454 wantJSON: "_users", 455 }, 456 { 457 name: "delete non-existent config key", 458 method: http.MethodDelete, 459 path: "/_node/_local/_config/foo/bar", 460 authUser: userAdmin, 461 wantStatus: http.StatusNotFound, 462 wantJSON: map[string]interface{}{ 463 "error": "not_found", 464 "reason": "unknown_config_value", 465 }, 466 }, 467 { 468 name: "set config not supported by config backend", 469 extraOptions: []Option{ 470 WithConfig(&readOnlyConfig{ 471 Config: config.Default(), 472 }), 473 }, 474 method: http.MethodPut, 475 path: "/_node/_local/_config/foo/bar", 476 body: strings.NewReader(`"oink"`), 477 authUser: userAdmin, 478 wantStatus: http.StatusMethodNotAllowed, 479 wantJSON: map[string]interface{}{ 480 "error": "method_not_allowed", 481 "reason": "configuration is read-only", 482 }, 483 }, 484 { 485 name: "delete config not supported by config backend", 486 extraOptions: []Option{ 487 WithConfig(&readOnlyConfig{ 488 Config: config.Default(), 489 }), 490 }, 491 method: http.MethodDelete, 492 path: "/_node/_local/_config/foo/bar", 493 authUser: userAdmin, 494 wantStatus: http.StatusMethodNotAllowed, 495 wantJSON: map[string]interface{}{ 496 "error": "method_not_allowed", 497 "reason": "configuration is read-only", 498 }, 499 }, 500 { 501 name: "too many uuids", 502 extraOptions: []Option{ 503 WithConfig(&readOnlyConfig{ 504 Config: config.Default(), 505 }), 506 }, 507 method: http.MethodGet, 508 path: "/_uuids?count=99999", 509 wantStatus: http.StatusBadRequest, 510 wantJSON: map[string]interface{}{ 511 "error": "bad_request", 512 "reason": "count must not exceed 1000", 513 }, 514 }, 515 { 516 name: "invalid count", 517 extraOptions: []Option{ 518 WithConfig(&readOnlyConfig{ 519 Config: config.Default(), 520 }), 521 }, 522 method: http.MethodGet, 523 path: "/_uuids?count=chicken", 524 wantStatus: http.StatusBadRequest, 525 wantJSON: map[string]interface{}{ 526 "error": "bad_request", 527 "reason": "count must be a positive integer", 528 }, 529 }, 530 { 531 name: "random uuids", 532 extraOptions: []Option{ 533 WithConfig(&readOnlyConfig{ 534 Config: config.Map( 535 map[string]map[string]string{ 536 "uuids": {"algorithm": "random"}, 537 }, 538 ), 539 }), 540 }, 541 method: http.MethodGet, 542 path: "/_uuids", 543 wantStatus: http.StatusOK, 544 target: new(struct { 545 UUIDs []string `json:"uuids" validate:"required,len=1,dive,required,len=32,hexadecimal"` 546 }), 547 }, 548 { 549 name: "many random uuids", 550 extraOptions: []Option{ 551 WithConfig(&readOnlyConfig{ 552 Config: config.Map( 553 map[string]map[string]string{ 554 "uuids": {"algorithm": "random"}, 555 }, 556 ), 557 }), 558 }, 559 method: http.MethodGet, 560 path: "/_uuids?count=10", 561 wantStatus: http.StatusOK, 562 target: new(struct { 563 UUIDs []string `json:"uuids" validate:"required,len=10,dive,required,len=32,hexadecimal"` 564 }), 565 }, 566 { 567 name: "sequential uuids", 568 extraOptions: []Option{ 569 WithConfig(&readOnlyConfig{ 570 Config: config.Default(), 571 }), 572 }, 573 method: http.MethodGet, 574 path: "/_uuids", 575 wantStatus: http.StatusOK, 576 target: new(struct { 577 UUIDs []string `json:"uuids" validate:"required,len=1,dive,required,len=32,hexadecimal"` 578 }), 579 }, 580 { 581 name: "many random uuids", 582 extraOptions: []Option{ 583 WithConfig(&readOnlyConfig{ 584 Config: config.Default(), 585 }), 586 }, 587 method: http.MethodGet, 588 path: "/_uuids?count=10", 589 wantStatus: http.StatusOK, 590 target: new(struct { 591 UUIDs []string `json:"uuids" validate:"required,len=10,dive,required,len=32,hexadecimal"` 592 }), 593 }, 594 { 595 name: "one utc random uuid", 596 extraOptions: []Option{ 597 WithConfig(&readOnlyConfig{ 598 Config: config.Map( 599 map[string]map[string]string{ 600 "uuids": {"algorithm": "utc_random"}, 601 }, 602 ), 603 }), 604 }, 605 method: http.MethodGet, 606 path: "/_uuids", 607 wantStatus: http.StatusOK, 608 target: new(struct { 609 UUIDs []string `json:"uuids" validate:"required,len=1,dive,required,len=32,hexadecimal"` 610 }), 611 }, 612 { 613 name: "10 utc random uuids", 614 extraOptions: []Option{ 615 WithConfig(&readOnlyConfig{ 616 Config: config.Map( 617 map[string]map[string]string{ 618 "uuids": {"algorithm": "utc_random"}, 619 }, 620 ), 621 }), 622 }, 623 method: http.MethodGet, 624 path: "/_uuids?count=10", 625 wantStatus: http.StatusOK, 626 target: new(struct { 627 UUIDs []string `json:"uuids" validate:"required,len=10,dive,required,len=32,hexadecimal"` 628 }), 629 }, 630 { 631 name: "one utc id uuid", 632 extraOptions: []Option{ 633 WithConfig(&readOnlyConfig{ 634 Config: config.Map( 635 map[string]map[string]string{ 636 "uuids": { 637 "algorithm": "utc_id", 638 "utc_id_suffix": "oink", 639 }, 640 }, 641 ), 642 }), 643 }, 644 method: http.MethodGet, 645 path: "/_uuids", 646 wantStatus: http.StatusOK, 647 target: new(struct { 648 UUIDs []string `json:"uuids" validate:"required,len=1,dive,required,len=18,endswith=oink"` 649 }), 650 }, 651 { 652 name: "10 utc id uuids", 653 extraOptions: []Option{ 654 WithConfig(&readOnlyConfig{ 655 Config: config.Map( 656 map[string]map[string]string{ 657 "uuids": { 658 "algorithm": "utc_id", 659 "utc_id_suffix": "oink", 660 }, 661 }, 662 ), 663 }), 664 }, 665 method: http.MethodGet, 666 path: "/_uuids?count=10", 667 wantStatus: http.StatusOK, 668 target: new(struct { 669 UUIDs []string `json:"uuids" validate:"required,len=10,dive,required,len=18,endswith=oink"` 670 }), 671 }, 672 { 673 name: "create db", 674 method: http.MethodPut, 675 path: "/db3", 676 authUser: userAdmin, 677 wantStatus: http.StatusCreated, 678 wantJSON: map[string]interface{}{ 679 "ok": true, 680 }, 681 }, 682 { 683 name: "delete db, not found", 684 method: http.MethodDelete, 685 path: "/db3", 686 authUser: userAdmin, 687 wantStatus: http.StatusNotFound, 688 wantJSON: map[string]interface{}{ 689 "error": "not_found", 690 "reason": "database does not exist", 691 }, 692 }, 693 { 694 name: "delete db", 695 method: http.MethodDelete, 696 path: "/db2", 697 authUser: userAdmin, 698 wantStatus: http.StatusOK, 699 wantJSON: map[string]interface{}{ 700 "ok": true, 701 }, 702 }, 703 { 704 name: "post document", 705 driver: "memory", 706 init: func(t *testing.T, client *kivik.Client) { //nolint:thelper // not a helper 707 if err := client.CreateDB(context.Background(), "db1", nil); err != nil { 708 t.Fatal(err) 709 } 710 }, 711 method: http.MethodPost, 712 path: "/db1", 713 body: strings.NewReader(`{"foo":"bar"}`), 714 authUser: userAdmin, 715 wantStatus: http.StatusCreated, 716 target: &struct { 717 ID string `json:"id" validate:"required,uuid"` 718 Rev string `json:"rev" validate:"required,startswith=1-"` 719 OK bool `json:"ok" validate:"required,eq=true"` 720 }{}, 721 }, 722 { 723 name: "get document", 724 method: http.MethodGet, 725 path: "/db1/foo", 726 authUser: userAdmin, 727 wantStatus: http.StatusOK, 728 wantJSON: map[string]interface{}{ 729 "_id": "foo", 730 "_rev": "1-beea34a62a215ab051862d1e5d93162e", 731 "foo": "bar", 732 }, 733 }, 734 { 735 name: "all dbs stats", 736 method: http.MethodGet, 737 path: "/_dbs_info", 738 authUser: userAdmin, 739 wantStatus: http.StatusOK, 740 wantJSON: []map[string]interface{}{ 741 { 742 "compact_running": false, 743 "data_size": 0, 744 "db_name": "bobsdb", 745 "disk_size": 0, 746 "doc_count": 0, 747 "doc_del_count": 0, 748 "update_seq": "", 749 }, 750 { 751 "compact_running": false, 752 "data_size": 0, 753 "db_name": "db1", 754 "disk_size": 0, 755 "doc_count": 0, 756 "doc_del_count": 0, 757 "update_seq": "", 758 }, 759 { 760 "compact_running": false, 761 "data_size": 0, 762 "db_name": "db2", 763 "disk_size": 0, 764 "doc_count": 0, 765 "doc_del_count": 0, 766 "update_seq": "", 767 }, 768 }, 769 }, 770 { 771 name: "dbs stats", 772 method: http.MethodPost, 773 path: "/_dbs_info", 774 authUser: userAdmin, 775 headers: map[string]string{"Content-Type": "application/json"}, 776 body: strings.NewReader(`{"keys":["db1","notfound"]}`), 777 wantStatus: http.StatusOK, 778 wantJSON: []map[string]interface{}{ 779 { 780 "compact_running": false, 781 "data_size": 0, 782 "db_name": "db1", 783 "disk_size": 0, 784 "doc_count": 0, 785 "doc_del_count": 0, 786 "update_seq": "", 787 }, 788 nil, 789 }, 790 }, 791 { 792 name: "get security", 793 method: http.MethodGet, 794 path: "/db1/_security", 795 authUser: userAdmin, 796 wantStatus: http.StatusOK, 797 wantJSON: map[string]interface{}{ 798 "admins": map[string]interface{}{ 799 "names": []string{"superuser"}, 800 "roles": []string{"admins"}, 801 }, 802 "members": map[string]interface{}{ 803 "names": []string{"user1", "user2"}, 804 "roles": []string{"developers"}, 805 }, 806 }, 807 }, 808 func() serverTest { 809 const want = `{"admins":{"names":["superuser"],"roles":["admins"]},"members":{"names":["user1","user2"],"roles":["developers"]}}` 810 return serverTest{ 811 name: "put security", 812 method: http.MethodPut, 813 path: "/db2/_security", 814 authUser: userAdmin, 815 headers: map[string]string{"Content-Type": "application/json"}, 816 body: strings.NewReader(want), 817 wantStatus: http.StatusOK, 818 wantJSON: map[string]interface{}{ 819 "ok": true, 820 }, 821 check: func(t *testing.T, client *kivik.Client) { //nolint:thelper // Not a helper 822 sec, err := client.DB("db2").Security(context.Background()) 823 if err != nil { 824 t.Fatal(err) 825 } 826 if d := testy.DiffAsJSON([]byte(want), sec); d != nil { 827 t.Errorf("Unexpected final result: %s", d) 828 } 829 }, 830 } 831 }(), 832 { 833 name: "put security, unauthorized", 834 method: http.MethodPut, 835 path: "/db2/_security", 836 headers: map[string]string{"Content-Type": "application/json"}, 837 body: strings.NewReader(`{"admins":{"names":["bob"]}}`), 838 wantStatus: http.StatusUnauthorized, 839 wantJSON: map[string]interface{}{ 840 "error": "unauthorized", 841 "reason": "User not authenticated", 842 }, 843 }, 844 { 845 name: "put security, no admin access", 846 method: http.MethodPut, 847 authUser: userBob, 848 path: "/db2/_security", 849 headers: map[string]string{"Content-Type": "application/json"}, 850 body: strings.NewReader(`{"admins":{"names":["bob"]}}`), 851 wantStatus: http.StatusForbidden, 852 wantJSON: map[string]interface{}{ 853 "error": "forbidden", 854 "reason": "User lacks sufficient privileges", 855 }, 856 }, 857 { 858 name: "put security, correct admin user", 859 method: http.MethodPut, 860 authUser: userErin, 861 path: "/bobsdb/_security", 862 headers: map[string]string{"Content-Type": "application/json"}, 863 body: strings.NewReader(`{"admins":{"names":["bob"]}}`), 864 wantStatus: http.StatusOK, 865 wantJSON: map[string]interface{}{ 866 "ok": true, 867 }, 868 }, 869 { 870 name: "put security, correct admin role", 871 method: http.MethodPut, 872 authUser: userFrank, 873 path: "/bobsdb/_security", 874 headers: map[string]string{"Content-Type": "application/json"}, 875 body: strings.NewReader(`{"admins":{"names":["bob"]}}`), 876 wantStatus: http.StatusOK, 877 wantJSON: map[string]interface{}{ 878 "ok": true, 879 }, 880 }, 881 { 882 name: "db info, unauthenticated", 883 method: http.MethodHead, 884 path: "/bobsdb", 885 wantStatus: http.StatusUnauthorized, 886 wantJSON: map[string]interface{}{ 887 "error": "unauthorized", 888 "reason": "User not authenticated", 889 }, 890 }, 891 { 892 name: "db info, authenticated wrong user, wrong role", 893 method: http.MethodHead, 894 authUser: userAlice, 895 path: "/bobsdb", 896 wantStatus: http.StatusForbidden, 897 wantJSON: map[string]interface{}{ 898 "error": "forbidden", 899 "reason": "User lacks sufficient privileges", 900 }, 901 }, 902 { 903 name: "db info, authenticated correct user", 904 method: http.MethodHead, 905 authUser: userBob, 906 path: "/bobsdb", 907 wantStatus: http.StatusOK, 908 }, 909 { 910 name: "db info, authenticated wrong role", 911 method: http.MethodHead, 912 authUser: userCharlie, 913 path: "/bobsdb", 914 wantStatus: http.StatusForbidden, 915 wantJSON: map[string]interface{}{ 916 "error": "forbidden", 917 "reason": "User lacks sufficient privileges", 918 }, 919 }, 920 { 921 name: "db info, authenticated correct role", 922 method: http.MethodHead, 923 authUser: userDavid, 924 path: "/bobsdb", 925 wantStatus: http.StatusOK, 926 }, 927 { 928 name: "db info, authenticated as admin user", 929 method: http.MethodHead, 930 authUser: userErin, 931 path: "/bobsdb", 932 wantStatus: http.StatusOK, 933 }, 934 { 935 name: "db info, authenticated as admin role", 936 method: http.MethodHead, 937 authUser: userFrank, 938 path: "/bobsdb", 939 wantStatus: http.StatusOK, 940 }, 941 } 942 943 tests.Run(t) 944 } 945 946 type readOnlyConfig struct { 947 config.Config 948 // To prevent the embedded methods from being accessible 949 SetKey int 950 Delete int 951 }