github.com/go-kivik/kivik/v4@v4.3.2/couchdb/replication_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 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 }