...

Source file src/github.com/go-kivik/kivik/v4/x/server/server_test.go

Documentation: github.com/go-kivik/kivik/v4/x/server

     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  }
   952  

View as plain text