...

Source file src/github.com/go-kivik/kivik/v4/couchdb/replication_test.go

Documentation: github.com/go-kivik/kivik/v4/couchdb

     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  package couchdb
    14  
    15  import (
    16  	"context"
    17  	"encoding/json"
    18  	"errors"
    19  	"fmt"
    20  	"net/http"
    21  	"testing"
    22  	"time"
    23  
    24  	"gitlab.com/flimzy/testy"
    25  
    26  	kivik "github.com/go-kivik/kivik/v4"
    27  	"github.com/go-kivik/kivik/v4/driver"
    28  	internal "github.com/go-kivik/kivik/v4/int/errors"
    29  	"github.com/go-kivik/kivik/v4/int/mock"
    30  )
    31  
    32  func TestReplicationError(t *testing.T) {
    33  	status := 404
    34  	reason := "not found"
    35  	err := &replicationError{status: status, reason: reason}
    36  	if d := internal.StatusErrorDiff(reason, status, err); d != "" {
    37  		t.Error(d)
    38  	}
    39  }
    40  
    41  func TestStateTime(t *testing.T) {
    42  	type stTest struct {
    43  		Name     string
    44  		Input    string
    45  		Error    string
    46  		Expected string
    47  	}
    48  	tests := []stTest{
    49  		{
    50  			Name:     "Blank",
    51  			Error:    "unexpected end of JSON input",
    52  			Expected: "0001-01-01 00:00:00 +0000",
    53  		},
    54  		{
    55  			Name:     "ValidRFC3339",
    56  			Input:    `"2011-02-17T20:22:02+01:00"`,
    57  			Expected: "2011-02-17 20:22:02 +0100",
    58  		},
    59  		{
    60  			Name:     "ValidUnixTimestamp",
    61  			Input:    "1492543959",
    62  			Expected: "2017-04-18 19:32:39 +0000",
    63  		},
    64  		{
    65  			Name:     "invalid timestamp",
    66  			Input:    `"foo"`,
    67  			Error:    `kivik: '"foo"' does not appear to be a valid timestamp`,
    68  			Expected: "0001-01-01 00:00:00 +0000",
    69  		},
    70  	}
    71  	for _, test := range tests {
    72  		func(test stTest) {
    73  			t.Run(test.Name, func(t *testing.T) {
    74  				var result replicationStateTime
    75  				err := json.Unmarshal([]byte(test.Input), &result)
    76  				if !testy.ErrorMatches(test.Error, err) {
    77  					t.Errorf("Unexpected error: %s", err)
    78  				}
    79  				if r := time.Time(result).Format("2006-01-02 15:04:05 -0700"); r != test.Expected {
    80  					t.Errorf("Result\nExpected: %s\n  Actual: %s\n", test.Expected, r)
    81  				}
    82  			})
    83  		}(test)
    84  	}
    85  }
    86  
    87  func TestReplicationErrorUnmarshal(t *testing.T) {
    88  	tests := []struct {
    89  		name     string
    90  		input    string
    91  		expected *replicationError
    92  		err      string
    93  	}{
    94  		{
    95  			name:  "doc example 1",
    96  			input: `"db_not_found: could not open http://adm:*****@localhost:5984/missing/"`,
    97  			expected: &replicationError{
    98  				status: http.StatusNotFound,
    99  				reason: "db_not_found: could not open http://adm:*****@localhost:5984/missing/",
   100  			},
   101  		},
   102  		{
   103  			name:  "timeout",
   104  			input: `"timeout: some timeout occurred"`,
   105  			expected: &replicationError{
   106  				status: http.StatusRequestTimeout,
   107  				reason: "timeout: some timeout occurred",
   108  			},
   109  		},
   110  		{
   111  			name:  "unknown",
   112  			input: `"unknown error"`,
   113  			expected: &replicationError{
   114  				status: http.StatusInternalServerError,
   115  				reason: "unknown error",
   116  			},
   117  		},
   118  		{
   119  			name:  "invalid JSON",
   120  			input: `"\C"`,
   121  			err:   "invalid character 'C' in string escape code",
   122  		},
   123  		{
   124  			name:  "Unauthorized",
   125  			input: `"unauthorized: unauthorized to access or create database foo"`,
   126  			expected: &replicationError{
   127  				status: http.StatusUnauthorized,
   128  				reason: "unauthorized: unauthorized to access or create database foo",
   129  			},
   130  		},
   131  	}
   132  	for _, test := range tests {
   133  		t.Run(test.name, func(t *testing.T) {
   134  			repErr := new(replicationError)
   135  			err := repErr.UnmarshalJSON([]byte(test.input))
   136  			if !testy.ErrorMatches(test.err, err) {
   137  				t.Errorf("Unexpected error: %s", err)
   138  			}
   139  			if err != nil {
   140  				return
   141  			}
   142  			if d := testy.DiffInterface(test.expected, repErr); d != nil {
   143  				t.Error(d)
   144  			}
   145  		})
   146  	}
   147  }
   148  
   149  func TestReplicate(t *testing.T) {
   150  	tests := []struct {
   151  		name           string
   152  		target, source string
   153  		options        kivik.Option
   154  		client         *client
   155  		status         int
   156  		err            string
   157  	}{
   158  		{
   159  			name:   "no target",
   160  			status: http.StatusBadRequest,
   161  			err:    "kivik: targetDSN required",
   162  		},
   163  		{
   164  			name:   "no source",
   165  			target: "foo",
   166  			status: http.StatusBadRequest,
   167  			err:    "kivik: sourceDSN required",
   168  		},
   169  		{
   170  			name: "invalid options",
   171  			client: func() *client {
   172  				client := newTestClient(nil, errors.New("net error"))
   173  				b := false
   174  				client.schedulerDetected = &b
   175  				return client
   176  			}(),
   177  			target: "foo", source: "bar",
   178  			options: kivik.Param("foo", make(chan int)),
   179  			status:  http.StatusBadRequest,
   180  			err:     `^Post "?http://example.com/_replicator"?: json: unsupported type: chan int$`,
   181  		},
   182  		{
   183  			name:   "network error",
   184  			target: "foo", source: "bar",
   185  			client: func() *client {
   186  				client := newTestClient(nil, errors.New("net error"))
   187  				b := false
   188  				client.schedulerDetected = &b
   189  				return client
   190  			}(),
   191  			status: http.StatusBadGateway,
   192  			err:    `Post "?http://example.com/_replicator"?: net error`,
   193  		},
   194  		{
   195  			name:   "1.6.1",
   196  			target: "foo", source: "bar",
   197  			client: func() *client {
   198  				client := newCustomClient(func(*http.Request) (*http.Response, error) {
   199  					return &http.Response{
   200  						StatusCode: 201,
   201  						Header: http.Header{
   202  							"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   203  							"Location":       {"http://localhost:5984/_replicator/4ab99e4d7d4b5a6c5a6df0d0ed01221d"},
   204  							"ETag":           {`"1-290800e5803500237075f9b08226cffd"`},
   205  							"Date":           {"Mon, 30 Oct 2017 20:03:34 GMT"},
   206  							"Content-Type":   {"application/json"},
   207  							"Content-Length": {"95"},
   208  							"Cache-Control":  {"must-revalidate"},
   209  						},
   210  						Body: Body(`{"ok":true,"id":"4ab99e4d7d4b5a6c5a6df0d0ed01221d","rev":"1-290800e5803500237075f9b08226cffd"}`),
   211  					}, nil
   212  				})
   213  				b := false
   214  				client.schedulerDetected = &b
   215  				return client
   216  			}(),
   217  		},
   218  		{
   219  			name:   "2.1.0",
   220  			target: "foo", source: "bar",
   221  			client: func() *client {
   222  				client := newCustomClient(func(req *http.Request) (*http.Response, error) {
   223  					switch req.URL.Path {
   224  					case "/_replicator":
   225  						return &http.Response{
   226  							StatusCode: 201,
   227  							Header: http.Header{
   228  								"Server":              {"CouchDB/2.1.0 (Erlang OTP/17)"},
   229  								"Location":            {"http://localhost:6002/_replicator/56d257bd2125c8f15870b3ddd2078b23"},
   230  								"Date":                {"Sat, 18 Nov 2017 11:13:58 GMT"},
   231  								"Content-Type":        {"application/json"},
   232  								"Content-Length":      {"95"},
   233  								"Cache-Control":       {"must-revalidate"},
   234  								"X-CouchDB-Body-Time": {"0"},
   235  								"X-Couch-Request-ID":  {"a97b982715"},
   236  							},
   237  							Body: Body(`{"ok":true,"id":"56d257bd2125c8f15870b3ddd2078b23","rev":"1-290800e5803500237075f9b08226cffd"}`),
   238  						}, nil
   239  					case "/_scheduler/docs/_replicator/56d257bd2125c8f15870b3ddd2078b23":
   240  						return &http.Response{
   241  							StatusCode: 200,
   242  							Header: http.Header{
   243  								"Server":         {"CouchDB/2.1.0 (Erlang OTP/17)"},
   244  								"Date":           {"Sat, 18 Nov 2017 11:18:33 GMT"},
   245  								"Content-Type":   {"application/json"},
   246  								"Content-Length": {"427"},
   247  								"Cache-Control":  {"must-revalidate"},
   248  							},
   249  							Body: Body(fmt.Sprintf(`{"database":"_replicator","doc_id":"56d257bd2125c8f15870b3ddd2078b23","id":null,"source":"foo","target":"bar","state":"failed","error_count":1,"info":"Replication %s specified by document %s already started, triggered by document %s from db %s","start_time":"2017-11-18T11:13:58Z","last_updated":"2017-11-18T11:13:58Z"}`, "`c636d089fbdc3a9a937a466acf8f42c3`", "`56d257bd2125c8f15870b3ddd2078b23`", "`56d257bd2125c8f15870b3ddd2074759`", "`_replicator`")),
   250  						}, nil
   251  					default:
   252  						return nil, fmt.Errorf("Unexpected path: %s", req.URL.Path)
   253  					}
   254  				})
   255  				b := true
   256  				client.schedulerDetected = &b
   257  				return client
   258  			}(),
   259  		},
   260  		{
   261  			name:   "scheduler detection error",
   262  			target: "foo", source: "bar",
   263  			client: newTestClient(nil, errors.New("sched err")),
   264  			status: http.StatusBadGateway,
   265  			err:    `Head "?http://example.com/_scheduler/jobs"?: sched err`,
   266  		},
   267  	}
   268  	for _, test := range tests {
   269  		t.Run(test.name, func(t *testing.T) {
   270  			opts := test.options
   271  			if opts == nil {
   272  				opts = mock.NilOption
   273  			}
   274  			resp, err := test.client.Replicate(context.Background(), test.target, test.source, opts)
   275  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   276  				t.Error(d)
   277  			}
   278  			if err != nil {
   279  				return
   280  			}
   281  			if _, ok := resp.(*replication); ok {
   282  				return
   283  			}
   284  			if _, ok := resp.(*schedulerReplication); ok {
   285  				return
   286  			}
   287  			t.Errorf("Unexpected response type: %T", resp)
   288  		})
   289  	}
   290  }
   291  
   292  func TestLegacyGetReplications(t *testing.T) {
   293  	tests := []struct {
   294  		name     string
   295  		options  map[string]interface{}
   296  		client   *client
   297  		expected []*replication
   298  		status   int
   299  		err      string
   300  	}{
   301  		{
   302  			name:    "invalid options",
   303  			options: map[string]interface{}{"foo": make(chan int)},
   304  			status:  http.StatusBadRequest,
   305  			err:     "kivik: invalid type chan int for options",
   306  		},
   307  		{
   308  			name:   "network error",
   309  			client: newTestClient(nil, errors.New("net error")),
   310  			status: http.StatusBadGateway,
   311  			err:    `^Get "?http://example.com/_replicator/_all_docs\?include_docs=true"?: net error$`,
   312  		},
   313  		{
   314  			name: "success, 1.6.1",
   315  			client: newTestClient(&http.Response{
   316  				StatusCode: 200,
   317  				Header: http.Header{
   318  					"Transfer-Encoding": {"chunked"},
   319  					"Server":            {"CouchDB/1.6.1 (Erlang OTP/17)"},
   320  					"ETag":              {`"97AGDUD7SV24L2PLSG3XG4MOY"`},
   321  					"Date":              {"Mon, 30 Oct 2017 20:31:31 GMT"},
   322  					"Content-Type":      {"application/json"},
   323  					"Cache-Control":     {"must-revalidate"},
   324  				},
   325  				Body: Body(`{"total_rows":2,"offset":0,"rows":[
   326  				{"id":"4ab99e4d7d4b5a6c5a6df0d0ed01221d","key":"4ab99e4d7d4b5a6c5a6df0d0ed01221d","value":{"rev":"2-6419706e969050d8000efad07259de4f"},"doc":{"_id":"4ab99e4d7d4b5a6c5a6df0d0ed01221d","_rev":"2-6419706e969050d8000efad07259de4f","source":"foo","target":"bar","owner":"admin","_replication_state":"error","_replication_state_time":"2017-10-30T20:03:34+00:00","_replication_state_reason":"unauthorized: unauthorized to access or create database foo","_replication_id":"548507fbb9fb9fcd8a3b27050b9ba5bf"}},
   327  				{"id":"_design/_replicator","key":"_design/_replicator","value":{"rev":"1-5bfa2c99eefe2b2eb4962db50aa3cfd4"},"doc":{"_id":"_design/_replicator","_rev":"1-5bfa2c99eefe2b2eb4962db50aa3cfd4","language":"javascript","validate_doc_update":"..."}}
   328  				]}`),
   329  			}, nil),
   330  			expected: []*replication{
   331  				{
   332  					docID:         "4ab99e4d7d4b5a6c5a6df0d0ed01221d",
   333  					replicationID: "548507fbb9fb9fcd8a3b27050b9ba5bf",
   334  					source:        "foo",
   335  					target:        "bar",
   336  					endTime:       parseTime(t, "2017-10-30T20:03:34+00:00"),
   337  					state:         "error",
   338  					err:           &replicationError{status: 401, reason: "unauthorized: unauthorized to access or create database foo"},
   339  				},
   340  			},
   341  		},
   342  	}
   343  	for _, test := range tests {
   344  		t.Run(test.name, func(t *testing.T) {
   345  			reps, err := test.client.legacyGetReplications(context.Background(), test.options)
   346  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   347  				t.Error(d)
   348  			}
   349  			if err != nil {
   350  				return
   351  			}
   352  			result := make([]*replication, len(reps))
   353  			for i, rep := range reps {
   354  				result[i] = rep.(*replication)
   355  				result[i].db = nil
   356  			}
   357  			if d := testy.DiffInterface(test.expected, result); d != nil {
   358  				t.Error(d)
   359  			}
   360  		})
   361  	}
   362  }
   363  
   364  func TestGetReplications(t *testing.T) {
   365  	tests := []struct {
   366  		name   string
   367  		client *client
   368  		status int
   369  		err    string
   370  	}{
   371  		{
   372  			name:   "network error",
   373  			client: newTestClient(nil, errors.New("net error")),
   374  			status: http.StatusBadGateway,
   375  			err:    `Head "?http://example.com/_scheduler/jobs"?: net error`,
   376  		},
   377  		{
   378  			name: "no scheduler",
   379  			client: func() *client {
   380  				client := newCustomClient(func(req *http.Request) (*http.Response, error) {
   381  					if req.URL.Path != "/_replicator/_all_docs" {
   382  						return nil, fmt.Errorf("unexpected request path: %s", req.URL.Path)
   383  					}
   384  					return &http.Response{
   385  						StatusCode: 404,
   386  						Request:    &http.Request{Method: "GET"},
   387  						Body:       Body(""),
   388  					}, nil
   389  				})
   390  				b := false
   391  				client.schedulerDetected = &b
   392  				return client
   393  			}(),
   394  			status: http.StatusNotFound,
   395  			err:    "Not Found",
   396  		},
   397  		{
   398  			name: "scheduler detected",
   399  			client: func() *client {
   400  				client := newCustomClient(func(req *http.Request) (*http.Response, error) {
   401  					if req.URL.Path != "/_scheduler/docs" {
   402  						return nil, fmt.Errorf("unexpected request path: %s", req.URL.Path)
   403  					}
   404  					return &http.Response{
   405  						StatusCode: 404,
   406  						Request:    &http.Request{Method: "GET"},
   407  						Body:       Body(""),
   408  					}, nil
   409  				})
   410  				b := true
   411  				client.schedulerDetected = &b
   412  				return client
   413  			}(),
   414  			status: http.StatusNotFound,
   415  			err:    "Not Found",
   416  		},
   417  	}
   418  	for _, test := range tests {
   419  		t.Run(test.name, func(t *testing.T) {
   420  			_, err := test.client.GetReplications(context.Background(), mock.NilOption)
   421  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   422  				t.Error(d)
   423  			}
   424  		})
   425  	}
   426  }
   427  
   428  func TestReplicationUpdate(t *testing.T) {
   429  	tests := []struct {
   430  		name     string
   431  		rep      *replication
   432  		expected *driver.ReplicationInfo
   433  		status   int
   434  		err      string
   435  	}{
   436  		{
   437  			name: "network error",
   438  			rep: &replication{
   439  				docID: "4ab99e4d7d4b5a6c5a6df0d0ed01221d",
   440  				db:    newTestDB(nil, errors.New("net error")),
   441  			},
   442  			status: http.StatusBadGateway,
   443  			err:    `Get "?http://example.com/testdb/4ab99e4d7d4b5a6c5a6df0d0ed01221d"?: net error`,
   444  		},
   445  		{
   446  			name: "no active reps 1.6.1",
   447  			rep: &replication{
   448  				docID: "4ab99e4d7d4b5a6c5a6df0d0ed01221d",
   449  				db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   450  					switch req.URL.Path {
   451  					case "/testdb/4ab99e4d7d4b5a6c5a6df0d0ed01221d":
   452  						return &http.Response{
   453  							StatusCode: 200,
   454  							Header: http.Header{
   455  								"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   456  								"ETag":           {`"2-6419706e969050d8000efad07259de4f"`},
   457  								"Date":           {"Mon, 30 Oct 2017 20:57:15 GMT"},
   458  								"Content-Type":   {"application/json"},
   459  								"Content-Length": {"359"},
   460  								"Cache-Control":  {"must-revalidate"},
   461  							},
   462  							Body: Body(`{"_id":"4ab99e4d7d4b5a6c5a6df0d0ed01221d","_rev":"2-6419706e969050d8000efad07259de4f","source":"foo","target":"bar","owner":"admin","_replication_state":"error","_replication_state_time":"2017-10-30T20:03:34+00:00","_replication_state_reason":"unauthorized: unauthorized to access or create database foo","_replication_id":"548507fbb9fb9fcd8a3b27050b9ba5bf"}`),
   463  						}, nil
   464  					case "/_active_tasks":
   465  						return &http.Response{
   466  							StatusCode: 200,
   467  							Header: http.Header{
   468  								"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   469  								"Date":           {"Mon, 30 Oct 2017 21:06:40 GMT"},
   470  								"Content-Type":   {"application/json"},
   471  								"Content-Length": {"3"},
   472  								"Cache-Control":  {"must-revalidate"},
   473  							},
   474  							Body: Body(`[]`),
   475  						}, nil
   476  					default:
   477  						panic("Unknown req path: " + req.URL.Path)
   478  					}
   479  				}),
   480  			},
   481  			expected: &driver.ReplicationInfo{},
   482  		},
   483  	}
   484  	for _, test := range tests {
   485  		t.Run(test.name, func(t *testing.T) {
   486  			result := new(driver.ReplicationInfo)
   487  			err := test.rep.Update(context.Background(), result)
   488  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   489  				t.Error(d)
   490  			}
   491  			if err != nil {
   492  				return
   493  			}
   494  			if d := testy.DiffInterface(test.expected, result); d != nil {
   495  				t.Error(d)
   496  			}
   497  		})
   498  	}
   499  }
   500  
   501  func TestReplicationDelete(t *testing.T) {
   502  	tests := []struct {
   503  		name   string
   504  		rep    *replication
   505  		status int
   506  		err    string
   507  	}{
   508  		{
   509  			name: "network error",
   510  			rep: &replication{
   511  				docID: "foo",
   512  				db:    newTestDB(nil, errors.New("net error")),
   513  			},
   514  			status: http.StatusBadGateway,
   515  			err:    `Head "?http://example.com/testdb/foo"?: net error`,
   516  		},
   517  		{
   518  			name: "delete network error",
   519  			rep: &replication{
   520  				docID: "4ab99e4d7d4b5a6c5a6df0d0ed01221d",
   521  				db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   522  					if req.Method == "HEAD" {
   523  						return &http.Response{
   524  							StatusCode: 200,
   525  							Header: http.Header{
   526  								"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   527  								"ETag":           {`"2-6419706e969050d8000efad07259de4f"`},
   528  								"Date":           {"Mon, 30 Oct 2017 21:14:46 GMT"},
   529  								"Content-Type":   {"application/json"},
   530  								"Content-Length": {"359"},
   531  								"Cache-Control":  {"must-revalidate"},
   532  							},
   533  							Body: Body(""),
   534  						}, nil
   535  					}
   536  					return nil, errors.New("delete error")
   537  				}),
   538  			},
   539  			status: http.StatusBadGateway,
   540  			err:    `^(Delete "?http://example.com/testdb/4ab99e4d7d4b5a6c5a6df0d0ed01221d\?rev=2-6419706e969050d8000efad07259de4f"?: )?delete error`,
   541  		},
   542  		{
   543  			name: "success, 1.6.1",
   544  			rep: &replication{
   545  				docID: "4ab99e4d7d4b5a6c5a6df0d0ed01221d",
   546  				db: newCustomDB(func(req *http.Request) (*http.Response, error) {
   547  					if req.Method == "HEAD" {
   548  						return &http.Response{
   549  							StatusCode: 200,
   550  							Header: http.Header{
   551  								"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   552  								"ETag":           {`"2-6419706e969050d8000efad07259de4f"`},
   553  								"Date":           {"Mon, 30 Oct 2017 21:14:46 GMT"},
   554  								"Content-Type":   {"application/json"},
   555  								"Content-Length": {"359"},
   556  								"Cache-Control":  {"must-revalidate"},
   557  							},
   558  							Body: Body(""),
   559  						}, nil
   560  					}
   561  					return &http.Response{
   562  						StatusCode: 200,
   563  						Header: http.Header{
   564  							"Server":         {"CouchDB/1.6.1 (Erlang OTP/17)"},
   565  							"ETag":           {`"3-2ae9fa6e1f8982a08c4a42b3943e67c5"`},
   566  							"Date":           {"Mon, 30 Oct 2017 21:29:43 GMT"},
   567  							"Content-Type":   {"application/json"},
   568  							"Content-Length": {"95"},
   569  							"Cache-Control":  {"must-revalidate"},
   570  						},
   571  						Body: Body(`{"ok":true,"id":"4ab99e4d7d4b5a6c5a6df0d0ed01221d","rev":"3-2ae9fa6e1f8982a08c4a42b3943e67c5"}`),
   572  					}, nil
   573  				}),
   574  			},
   575  		},
   576  	}
   577  	for _, test := range tests {
   578  		t.Run(test.name, func(t *testing.T) {
   579  			err := test.rep.Delete(context.Background())
   580  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   581  				t.Error(d)
   582  			}
   583  		})
   584  	}
   585  }
   586  
   587  func TestUpdateActiveTasks(t *testing.T) {
   588  	tests := []struct {
   589  		name     string
   590  		rep      *replication
   591  		expected *activeTask
   592  		status   int
   593  		err      string
   594  	}{
   595  		{
   596  			name: "network error",
   597  			rep: &replication{
   598  				db: newTestDB(nil, errors.New("net error")),
   599  			},
   600  			status: http.StatusBadGateway,
   601  			err:    `Get "?http://example.com/_active_tasks"?: net error`,
   602  		},
   603  		{
   604  			name: "error response",
   605  			rep: &replication{
   606  				db: newTestDB(&http.Response{
   607  					StatusCode: 500,
   608  					Request:    &http.Request{Method: "GET"},
   609  					Body:       Body(""),
   610  				}, nil),
   611  			},
   612  			status: http.StatusInternalServerError,
   613  			err:    "Internal Server Error",
   614  		},
   615  		{
   616  			name: "invalid json response",
   617  			rep: &replication{
   618  				db: newTestDB(&http.Response{
   619  					StatusCode: 200,
   620  					Body:       Body("invalid json"),
   621  				}, nil),
   622  			},
   623  			status: http.StatusBadGateway,
   624  			err:    "invalid character 'i' looking for beginning of value",
   625  		},
   626  		{
   627  			name: "rep not found",
   628  			rep: &replication{
   629  				replicationID: "foo",
   630  				db: newTestDB(&http.Response{
   631  					StatusCode: 200,
   632  					Body:       Body("[]"),
   633  				}, nil),
   634  			},
   635  			status: http.StatusNotFound,
   636  			err:    "task not found",
   637  		},
   638  		{
   639  			name: "rep found",
   640  			rep: &replication{
   641  				replicationID: "foo",
   642  				db: newTestDB(&http.Response{
   643  					StatusCode: 200,
   644  					Body: Body(`[
   645  						{"type":"foo"},
   646  						{"type":"replication","replication_id":"unf"},
   647  						{"type":"replication","replication_id":"foo","docs_written":1}
   648  					]`),
   649  				}, nil),
   650  			},
   651  			expected: &activeTask{
   652  				Type:          "replication",
   653  				ReplicationID: "foo",
   654  				DocsWritten:   1,
   655  			},
   656  		},
   657  	}
   658  	for _, test := range tests {
   659  		t.Run(test.name, func(t *testing.T) {
   660  			result, err := test.rep.updateActiveTasks(context.Background())
   661  			if d := internal.StatusErrorDiffRE(test.err, test.status, err); d != "" {
   662  				t.Error(d)
   663  			}
   664  			if d := testy.DiffInterface(test.expected, result); d != nil {
   665  				t.Error(d)
   666  			}
   667  		})
   668  	}
   669  }
   670  
   671  func TestSetFromReplicatorDoc(t *testing.T) {
   672  	tests := []struct {
   673  		name     string
   674  		rep      *replication
   675  		doc      *replicatorDoc
   676  		expected *replication
   677  	}{
   678  		{
   679  			name: "started",
   680  			rep:  &replication{},
   681  			doc: &replicatorDoc{
   682  				State:     string(kivik.ReplicationStarted),
   683  				StateTime: replicationStateTime(parseTime(t, "2017-01-01T01:01:01Z")),
   684  			},
   685  			expected: &replication{
   686  				state:     "triggered",
   687  				startTime: parseTime(t, "2017-01-01T01:01:01Z"),
   688  			},
   689  		},
   690  		{
   691  			name: "errored",
   692  			rep:  &replication{},
   693  			doc: &replicatorDoc{
   694  				State:     string(kivik.ReplicationError),
   695  				StateTime: replicationStateTime(parseTime(t, "2017-01-01T01:01:01Z")),
   696  			},
   697  			expected: &replication{
   698  				state:   "error",
   699  				endTime: parseTime(t, "2017-01-01T01:01:01Z"),
   700  			},
   701  		},
   702  		{
   703  			name: "completed",
   704  			rep:  &replication{},
   705  			doc: &replicatorDoc{
   706  				State:     string(kivik.ReplicationComplete),
   707  				StateTime: replicationStateTime(parseTime(t, "2017-01-01T01:01:01Z")),
   708  			},
   709  			expected: &replication{
   710  				state:   "completed",
   711  				endTime: parseTime(t, "2017-01-01T01:01:01Z"),
   712  			},
   713  		},
   714  		{
   715  			name: "set fields",
   716  			rep:  &replication{},
   717  			doc: &replicatorDoc{
   718  				Source:        "foo",
   719  				Target:        "bar",
   720  				ReplicationID: "oink",
   721  				Error:         &replicationError{status: 500, reason: "unf"},
   722  			},
   723  			expected: &replication{
   724  				source:        "foo",
   725  				target:        "bar",
   726  				replicationID: "oink",
   727  				err:           &replicationError{status: 500, reason: "unf"},
   728  			},
   729  		},
   730  		{
   731  			name: "validate that existing fields aren't re-set",
   732  			rep:  &replication{source: "a", target: "b", replicationID: "c", err: errors.New("foo")},
   733  			doc: &replicatorDoc{
   734  				Source:        "foo",
   735  				Target:        "bar",
   736  				ReplicationID: "oink",
   737  			},
   738  			expected: &replication{
   739  				source:        "a",
   740  				target:        "b",
   741  				replicationID: "c",
   742  			},
   743  		},
   744  	}
   745  	for _, test := range tests {
   746  		t.Run(test.name, func(t *testing.T) {
   747  			test.rep.setFromReplicatorDoc(test.doc)
   748  			if d := testy.DiffInterface(test.expected, test.rep); d != nil {
   749  				t.Error(d)
   750  			}
   751  		})
   752  	}
   753  }
   754  
   755  func TestReplicationGetters(t *testing.T) {
   756  	const (
   757  		repID   = "a"
   758  		source  = "b"
   759  		target  = "c"
   760  		state   = "d"
   761  		wantErr = "e"
   762  	)
   763  	start := parseTime(t, "2017-01-01T01:01:01Z")
   764  	end := parseTime(t, "2017-01-01T01:01:02Z")
   765  	rep := &replication{
   766  		replicationID: repID,
   767  		source:        source,
   768  		target:        target,
   769  		startTime:     start,
   770  		endTime:       end,
   771  		state:         state,
   772  		err:           errors.New(wantErr),
   773  	}
   774  	if result := rep.ReplicationID(); result != repID {
   775  		t.Errorf("Unexpected replication ID: %s", result)
   776  	}
   777  	if result := rep.Source(); result != source {
   778  		t.Errorf("Unexpected source: %s", result)
   779  	}
   780  	if result := rep.Target(); result != target {
   781  		t.Errorf("Unexpected target: %s", result)
   782  	}
   783  	if result := rep.StartTime(); !result.Equal(start) {
   784  		t.Errorf("Unexpected start time: %v", result)
   785  	}
   786  	if result := rep.EndTime(); !result.Equal(end) {
   787  		t.Errorf("Unexpected end time: %v", result)
   788  	}
   789  	if result := rep.State(); result != state {
   790  		t.Errorf("Unexpected state: %s", result)
   791  	}
   792  	if err := rep.Err(); !testy.ErrorMatches(wantErr, err) {
   793  		t.Errorf("Unexpected error: %s", err)
   794  	}
   795  }
   796  

View as plain text