github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/sharing/replicator_test.go (about) 1 package sharing 2 3 import ( 4 "strings" 5 "testing" 6 "time" 7 8 "github.com/cozy/cozy-stack/model/instance" 9 "github.com/cozy/cozy-stack/pkg/config/config" 10 "github.com/cozy/cozy-stack/pkg/consts" 11 "github.com/cozy/cozy-stack/pkg/couchdb" 12 "github.com/cozy/cozy-stack/pkg/couchdb/revision" 13 "github.com/cozy/cozy-stack/tests/testutils" 14 "github.com/gofrs/uuid/v5" 15 "github.com/stretchr/testify/assert" 16 ) 17 18 // Some doctypes for the tests 19 const testDoctype = "io.cozy.sharing.tests" 20 const foos = "io.cozy.sharing.test.foos" 21 const bars = "io.cozy.sharing.test.bars" 22 const bazs = "io.cozy.sharing.test.bazs" 23 24 func TestReplicator(t *testing.T) { 25 if testing.Short() { 26 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 27 } 28 29 config.UseTestFile(t) 30 testutils.NeedCouchdb(t) 31 setup := testutils.NewSetup(t, t.Name()) 32 inst := setup.GetTestInstance() 33 34 t.Run("SequenceNumber", func(t *testing.T) { 35 // Start with an empty io.cozy.shared database 36 _ = couchdb.DeleteDB(inst, consts.Shared) 37 _ = couchdb.CreateDB(inst, consts.Shared) 38 39 s := &Sharing{SID: uuidv7(), Members: []Member{ 40 {Status: MemberStatusOwner, Name: "Alice"}, 41 {Status: MemberStatusReady, Name: "Bob"}, 42 }} 43 nb := 5 44 for i := 0; i < nb; i++ { 45 createASharedRef(t, inst, s.SID) 46 } 47 m := &s.Members[1] 48 49 rid, err := s.replicationID(m) 50 assert.NoError(t, err) 51 assert.Equal(t, "sharing-"+s.SID+"-1", rid) 52 53 seq, err := s.getLastSeqNumber(inst, m, "replicator") 54 assert.NoError(t, err) 55 assert.Empty(t, seq) 56 feed, err := s.callChangesFeed(inst, seq) 57 assert.NoError(t, err) 58 assert.NotEmpty(t, feed.Seq) 59 assert.Equal(t, nb, revision.Generation(feed.Seq)) 60 err = s.UpdateLastSequenceNumber(inst, m, "replicator", feed.Seq) 61 assert.NoError(t, err) 62 63 seqU, err := s.getLastSeqNumber(inst, m, "upload") 64 assert.NoError(t, err) 65 assert.Empty(t, seqU) 66 err = s.UpdateLastSequenceNumber(inst, m, "upload", "2-abc") 67 assert.NoError(t, err) 68 69 seq2, err := s.getLastSeqNumber(inst, m, "replicator") 70 assert.NoError(t, err) 71 assert.Equal(t, feed.Seq, seq2) 72 73 err = s.UpdateLastSequenceNumber(inst, m, "replicator", "2-abc") 74 assert.NoError(t, err) 75 seq3, err := s.getLastSeqNumber(inst, m, "replicator") 76 assert.NoError(t, err) 77 assert.Equal(t, feed.Seq, seq3) 78 }) 79 80 t.Run("InitialIndex", func(t *testing.T) { 81 // Start with an empty io.cozy.shared database 82 _ = couchdb.DeleteDB(inst, consts.Shared) 83 if err := couchdb.CreateDB(inst, consts.Shared); err != nil { 84 time.Sleep(1 * time.Second) 85 _ = couchdb.CreateDB(inst, consts.Shared) 86 } 87 88 // Create some documents that are not shared 89 for i := 0; i < 10; i++ { 90 id := uuidv7() 91 createDoc(t, inst, testDoctype, id, map[string]interface{}{"foo": id}) 92 } 93 94 s := Sharing{SID: uuidv7()} 95 96 // Rule 0 is local => no copy of documents 97 settingsDocID := uuidv7() 98 createDoc(t, inst, consts.Settings, settingsDocID, map[string]interface{}{"foo": settingsDocID}) 99 s.Rules = append(s.Rules, Rule{ 100 Title: "A local rule", 101 DocType: consts.Settings, 102 Values: []string{settingsDocID}, 103 Local: true, 104 }) 105 assert.NoError(t, s.InitialIndex(inst, s.Rules[len(s.Rules)-1], len(s.Rules)-1)) 106 nbShared := 0 107 assertNbSharedRef(t, inst, nbShared) 108 109 // Rule 1 is a unique shared document 110 oneID := uuidv7() 111 oneDoc := createDoc(t, inst, testDoctype, oneID, map[string]interface{}{"foo": "quuuuux"}) 112 s.Rules = append(s.Rules, Rule{ 113 Title: "A unique document", 114 DocType: testDoctype, 115 Values: []string{oneID}, 116 }) 117 assert.NoError(t, s.InitialIndex(inst, s.Rules[len(s.Rules)-1], len(s.Rules)-1)) 118 nbShared++ 119 assertNbSharedRef(t, inst, nbShared) 120 oneRef := getSharedRef(t, inst, testDoctype, oneID) 121 assert.NotNil(t, oneRef) 122 assert.Equal(t, testDoctype+"/"+oneID, oneRef.SID) 123 assert.Equal(t, &RevsTree{Rev: oneDoc.Rev()}, oneRef.Revisions) 124 assert.Contains(t, oneRef.Infos, s.SID) 125 assert.Equal(t, 1, oneRef.Infos[s.SID].Rule) 126 127 // Rule 2 is with a selector 128 twoIDs := []string{uuidv7(), uuidv7(), uuidv7()} 129 for _, id := range twoIDs { 130 createDoc(t, inst, testDoctype, id, map[string]interface{}{"foo": "bar"}) 131 } 132 s.Rules = append(s.Rules, Rule{ 133 Title: "the foo: bar documents", 134 DocType: testDoctype, 135 Selector: "foo", 136 Values: []string{"bar"}, 137 }) 138 assert.NoError(t, s.InitialIndex(inst, s.Rules[len(s.Rules)-1], len(s.Rules)-1)) 139 nbShared += len(twoIDs) 140 assertNbSharedRef(t, inst, nbShared) 141 for _, id := range twoIDs { 142 twoRef := getSharedRef(t, inst, testDoctype, id) 143 assert.NotNil(t, twoRef) 144 assert.Contains(t, twoRef.Infos, s.SID) 145 assert.Equal(t, 2, twoRef.Infos[s.SID].Rule) 146 } 147 148 // Rule 3 is another rule with a selector 149 threeIDs := []string{uuidv7(), uuidv7(), uuidv7()} 150 for i, id := range threeIDs { 151 u := "u" 152 for j := 0; j < i; j++ { 153 u += "u" 154 } 155 createDoc(t, inst, testDoctype, id, map[string]interface{}{"foo": "q" + u + "x"}) 156 } 157 s.Rules = append(s.Rules, Rule{ 158 Title: "the foo: baz documents", 159 DocType: testDoctype, 160 Selector: "foo", 161 Values: []string{"qux", "quux", "quuux"}, 162 }) 163 assert.NoError(t, s.InitialIndex(inst, s.Rules[len(s.Rules)-1], len(s.Rules)-1)) 164 nbShared += len(threeIDs) 165 assertNbSharedRef(t, inst, nbShared) 166 for _, id := range threeIDs { 167 threeRef := getSharedRef(t, inst, testDoctype, id) 168 assert.NotNil(t, threeRef) 169 assert.Contains(t, threeRef.Infos, s.SID) 170 assert.Equal(t, 3, threeRef.Infos[s.SID].Rule) 171 } 172 173 // Another member accepts the sharing 174 for r, rule := range s.Rules { 175 assert.NoError(t, s.InitialIndex(inst, rule, r)) 176 } 177 assertNbSharedRef(t, inst, nbShared) 178 179 // A document is added 180 addID := uuidv7() 181 twoIDs = append(twoIDs, addID) 182 createDoc(t, inst, testDoctype, addID, map[string]interface{}{"foo": "bar"}) 183 184 // A document is updated 185 updateID := twoIDs[0] 186 updateRef := getSharedRef(t, inst, testDoctype, updateID) 187 updateRev := updateRef.Revisions.Rev 188 updateDoc := updateDoc(t, inst, testDoctype, updateID, updateRev, map[string]interface{}{"foo": "bar", "updated": true}) 189 190 // A third member accepts the sharing 191 for r, rule := range s.Rules { 192 assert.NoError(t, s.InitialIndex(inst, rule, r)) 193 } 194 nbShared++ 195 assertNbSharedRef(t, inst, nbShared) 196 for _, id := range twoIDs { 197 twoRef := getSharedRef(t, inst, testDoctype, id) 198 assert.NotNil(t, twoRef) 199 assert.Contains(t, twoRef.Infos, s.SID) 200 assert.Equal(t, 2, twoRef.Infos[s.SID].Rule) 201 if id == updateID { 202 assert.Equal(t, updateRev, twoRef.Revisions.Rev) 203 assert.Equal(t, updateDoc.Rev(), twoRef.Revisions.Branches[0].Rev) 204 } 205 } 206 207 // Another sharing 208 s2 := Sharing{SID: uuidv7()} 209 s2.Rules = append(s2.Rules, Rule{ 210 Title: "the foo: baz documents", 211 DocType: testDoctype, 212 Selector: "foo", 213 Values: []string{"qux", "quux", "quuux"}, 214 }) 215 assert.NoError(t, s2.InitialIndex(inst, s2.Rules[len(s2.Rules)-1], len(s2.Rules)-1)) 216 assertNbSharedRef(t, inst, nbShared) 217 for _, id := range threeIDs { 218 threeRef := getSharedRef(t, inst, testDoctype, id) 219 assert.NotNil(t, threeRef) 220 assert.Contains(t, threeRef.Infos, s.SID) 221 assert.Equal(t, 3, threeRef.Infos[s.SID].Rule) 222 assert.Contains(t, threeRef.Infos, s2.SID) 223 assert.Equal(t, 0, threeRef.Infos[s2.SID].Rule) 224 } 225 }) 226 227 t.Run("CallChangesFeed", func(t *testing.T) { 228 // Start with an empty io.cozy.shared database 229 _ = couchdb.DeleteDB(inst, consts.Shared) 230 _ = couchdb.CreateDB(inst, consts.Shared) 231 232 foobars := "io.cozy.tests.foobars" 233 id1 := uuidv7() 234 id2 := uuidv7() 235 s := Sharing{ 236 SID: uuidv7(), 237 Rules: []Rule{ 238 { 239 Title: "foobars rule", 240 DocType: foobars, 241 Values: []string{id1, id2}, 242 }, 243 }, 244 } 245 ref1 := createSharedRef(t, inst, s.SID, foobars+"/"+id1, []string{"1-aaa"}) 246 ref2 := createSharedRef(t, inst, s.SID, foobars+"/"+id2, []string{"3-bbb"}) 247 appendRevisionToSharedRef(t, inst, ref1, "2-ccc") 248 249 feed, err := s.callChangesFeed(inst, "") 250 assert.NoError(t, err) 251 assert.NotEmpty(t, feed.Seq) 252 assert.Equal(t, 3, revision.Generation(feed.Seq)) 253 changes := &feed.Changes 254 assert.Equal(t, []string{"1-aaa", "2-ccc"}, changes.Changed[ref1.SID]) 255 assert.Equal(t, []string{"3-bbb"}, changes.Changed[ref2.SID]) 256 expected := map[string]int{ 257 foobars + "/" + id1: 0, 258 foobars + "/" + id2: 0, 259 } 260 assert.Equal(t, expected, feed.RuleIndexes) 261 assert.False(t, feed.Pending) 262 263 feed2, err := s.callChangesFeed(inst, feed.Seq) 264 assert.NoError(t, err) 265 assert.Equal(t, feed.Seq, feed2.Seq) 266 changes = &feed2.Changes 267 assert.Empty(t, changes.Changed) 268 269 appendRevisionToSharedRef(t, inst, ref1, "3-ddd") 270 feed3, err := s.callChangesFeed(inst, feed.Seq) 271 assert.NoError(t, err) 272 assert.NotEmpty(t, feed3.Seq) 273 assert.Equal(t, 4, revision.Generation(feed3.Seq)) 274 changes = &feed3.Changes 275 assert.Equal(t, []string{"1-aaa", "2-ccc", "3-ddd"}, changes.Changed[ref1.SID]) 276 assert.NotContains(t, changes.Changed, ref2.SID) 277 }) 278 279 t.Run("GetMissingDocs", func(t *testing.T) { 280 hellos := "io.cozy.tests.hellos" 281 _ = couchdb.CreateDB(inst, hellos) 282 283 id1 := uuidv7() 284 doc1 := createDoc(t, inst, hellos, id1, map[string]interface{}{"hello": id1}) 285 id2 := uuidv7() 286 doc2 := createDoc(t, inst, hellos, id2, map[string]interface{}{"hello": id2}) 287 doc2b := updateDoc(t, inst, hellos, id2, doc2.Rev(), map[string]interface{}{"hello": id2, "bis": true}) 288 id3 := uuidv7() 289 doc3 := createDoc(t, inst, hellos, id3, map[string]interface{}{"hello": id3}) 290 doc3b := updateDoc(t, inst, hellos, id3, doc3.Rev(), map[string]interface{}{"hello": id3, "bis": true}) 291 s := Sharing{ 292 SID: uuidv7(), 293 Rules: []Rule{ 294 { 295 Title: "hellos rule", 296 DocType: hellos, 297 Values: []string{id1, id2, id3}, 298 }, 299 }, 300 } 301 302 missings := &Missings{ 303 hellos + "/" + id1: MissingEntry{ 304 Missing: []string{doc1.Rev()}, 305 }, 306 hellos + "/" + id2: MissingEntry{ 307 Missing: []string{doc2.Rev(), doc2b.Rev()}, 308 }, 309 hellos + "/" + id3: MissingEntry{ 310 Missing: []string{doc3b.Rev()}, 311 }, 312 } 313 changes := &Changes{ 314 Changed: make(Changed), 315 Removed: make(Removed), 316 } 317 results, err := s.getMissingDocs(inst, missings, changes) 318 assert.NoError(t, err) 319 assert.Contains(t, *results, hellos) 320 assert.Len(t, (*results)[hellos], 4) 321 322 var one, two, twob, three map[string]interface{} 323 for i, doc := range (*results)[hellos] { 324 switch doc["_id"] { 325 case id1: 326 one = (*results)[hellos][i] 327 case id2: 328 if _, ok := doc["bis"]; ok { 329 twob = (*results)[hellos][i] 330 } else { 331 two = (*results)[hellos][i] 332 } 333 case id3: 334 three = (*results)[hellos][i] 335 } 336 } 337 assert.NotNil(t, twob) 338 assert.NotNil(t, three) 339 340 assert.NotNil(t, one) 341 assert.Equal(t, doc1.Rev(), one["_rev"]) 342 assert.Equal(t, id1, one["hello"]) 343 assert.Equal(t, float64(1), one["_revisions"].(map[string]interface{})["start"]) 344 assert.Equal(t, stripGenerations(doc1.Rev()), one["_revisions"].(map[string]interface{})["ids"]) 345 346 assert.NotNil(t, two) 347 assert.Equal(t, doc2.Rev(), two["_rev"]) 348 assert.Equal(t, id2, two["hello"]) 349 assert.Equal(t, float64(1), two["_revisions"].(map[string]interface{})["start"]) 350 assert.Equal(t, stripGenerations(doc2.Rev()), two["_revisions"].(map[string]interface{})["ids"]) 351 352 assert.NotNil(t, twob) 353 assert.Equal(t, doc2b.Rev(), twob["_rev"]) 354 assert.Equal(t, id2, twob["hello"]) 355 assert.Equal(t, float64(2), twob["_revisions"].(map[string]interface{})["start"]) 356 assert.Equal(t, stripGenerations(doc2b.Rev(), doc2.Rev()), twob["_revisions"].(map[string]interface{})["ids"]) 357 358 assert.NotNil(t, three) 359 assert.Equal(t, doc3b.Rev(), three["_rev"]) 360 assert.Equal(t, id3, three["hello"]) 361 assert.Equal(t, float64(2), three["_revisions"].(map[string]interface{})["start"]) 362 assert.Equal(t, stripGenerations(doc3b.Rev(), doc3.Rev()), three["_revisions"].(map[string]interface{})["ids"]) 363 }) 364 365 t.Run("ApplyBulkDocs", func(t *testing.T) { 366 // Start with an empty io.cozy.shared database 367 _ = couchdb.DeleteDB(inst, consts.Shared) 368 _ = couchdb.CreateDB(inst, consts.Shared) 369 _ = couchdb.CreateDB(inst, foos) 370 371 s := Sharing{ 372 SID: uuidv7(), 373 Rules: []Rule{ 374 { 375 Title: "foos rule", 376 DocType: foos, 377 Selector: "hello", 378 Values: []string{"world"}, 379 }, 380 { 381 Title: "bars rule", 382 DocType: bars, 383 Selector: "hello", 384 Values: []string{"world"}, 385 }, 386 { 387 Title: "bazs rule", 388 DocType: bazs, 389 Selector: "hello", 390 Values: []string{"world"}, 391 }, 392 }, 393 } 394 s2 := Sharing{ 395 SID: uuidv7(), 396 Rules: []Rule{ 397 { 398 Title: "bars rule", 399 DocType: bars, 400 Selector: "hello", 401 Values: []string{"world"}, 402 }, 403 }, 404 } 405 406 // Add a new document 407 fooOneID := uuidv7() 408 payload := DocsByDoctype{ 409 foos: DocsList{ 410 { 411 "_id": fooOneID, 412 "_rev": "1-abc", 413 "_revisions": map[string]interface{}{ 414 "start": float64(1), 415 "ids": []interface{}{"abc"}, 416 }, 417 "hello": "world", 418 "number": "one", 419 }, 420 }, 421 } 422 err := s.ApplyBulkDocs(inst, payload) 423 assert.NoError(t, err) 424 nbShared := 1 425 assertNbSharedRef(t, inst, nbShared) 426 doc := getDoc(t, inst, foos, fooOneID) 427 assert.Equal(t, "1-abc", doc.Rev()) 428 assert.Equal(t, "one", doc.Get("number")) 429 ref := getSharedRef(t, inst, foos, fooOneID) 430 assert.Equal(t, &RevsTree{Rev: "1-abc"}, ref.Revisions) 431 assert.Contains(t, ref.Infos, s.SID) 432 assert.Equal(t, 0, ref.Infos[s.SID].Rule) 433 434 // Update a document 435 payload = DocsByDoctype{ 436 foos: DocsList{ 437 { 438 "_id": fooOneID, 439 "_rev": "2-def", 440 "_revisions": map[string]interface{}{ 441 "start": float64(2), 442 "ids": []interface{}{"def", "abc"}, 443 }, 444 "hello": "world", 445 "number": "one bis", 446 }, 447 }, 448 } 449 err = s.ApplyBulkDocs(inst, payload) 450 assert.NoError(t, err) 451 assertNbSharedRef(t, inst, nbShared) 452 doc = getDoc(t, inst, foos, fooOneID) 453 assert.Equal(t, "2-def", doc.Rev()) 454 assert.Equal(t, "one bis", doc.Get("number")) 455 ref = getSharedRef(t, inst, foos, fooOneID) 456 expected := &RevsTree{ 457 Rev: "1-abc", 458 Branches: []RevsTree{ 459 {Rev: "2-def"}, 460 }, 461 } 462 assert.Equal(t, expected, ref.Revisions) 463 assert.Contains(t, ref.Infos, s.SID) 464 assert.Equal(t, 0, ref.Infos[s.SID].Rule) 465 466 // Create a reference for another sharing, on a database that does not exist 467 barZeroID := uuidv7() 468 payload = DocsByDoctype{ 469 bars: DocsList{ 470 { 471 "_id": barZeroID, 472 "_rev": "1-111", 473 "_revisions": map[string]interface{}{ 474 "start": float64(1), 475 "ids": []interface{}{"111"}, 476 }, 477 "hello": "world", 478 "number": "zero", 479 }, 480 }, 481 } 482 err = s2.ApplyBulkDocs(inst, payload) 483 assert.NoError(t, err) 484 nbShared++ 485 assertNbSharedRef(t, inst, nbShared) 486 doc = getDoc(t, inst, bars, barZeroID) 487 assert.Equal(t, "1-111", doc.Rev()) 488 assert.Equal(t, "zero", doc.Get("number")) 489 ref = getSharedRef(t, inst, bars, barZeroID) 490 assert.Equal(t, &RevsTree{Rev: "1-111"}, ref.Revisions) 491 assert.Contains(t, ref.Infos, s2.SID) 492 assert.Equal(t, 0, ref.Infos[s2.SID].Rule) 493 494 // Add documents for two doctypes at the same time 495 barTwoID := uuidv7() 496 bazThreeID := uuidv7() 497 bazFourID := uuidv7() 498 payload = DocsByDoctype{ 499 bars: DocsList{ 500 { 501 "_id": barTwoID, 502 "_rev": "2-caa", 503 "_revisions": map[string]interface{}{ 504 "start": float64(2), 505 "ids": []interface{}{"caa", "baa"}, 506 }, 507 "hello": "world", 508 "number": "two", 509 }, 510 }, 511 bazs: DocsList{ 512 { 513 "_id": bazThreeID, 514 "_rev": "1-ddd", 515 "_revisions": map[string]interface{}{ 516 "start": float64(1), 517 "ids": []interface{}{"ddd"}, 518 }, 519 "hello": "world", 520 "number": "three", 521 }, 522 { 523 "_id": bazFourID, 524 "_rev": "1-eee", 525 "_revisions": map[string]interface{}{ 526 "start": float64(1), 527 "ids": []interface{}{"eee"}, 528 }, 529 "hello": "world", 530 "number": "four", 531 }, 532 }, 533 } 534 err = s.ApplyBulkDocs(inst, payload) 535 assert.NoError(t, err) 536 nbShared += 3 537 assertNbSharedRef(t, inst, nbShared) 538 doc = getDoc(t, inst, bars, barTwoID) 539 assert.Equal(t, "2-caa", doc.Rev()) 540 assert.Equal(t, "two", doc.Get("number")) 541 ref = getSharedRef(t, inst, bars, barTwoID) 542 assert.Equal(t, &RevsTree{Rev: "2-caa"}, ref.Revisions) 543 assert.Contains(t, ref.Infos, s.SID) 544 assert.Equal(t, 1, ref.Infos[s.SID].Rule) 545 doc = getDoc(t, inst, bazs, bazThreeID) 546 assert.Equal(t, "1-ddd", doc.Rev()) 547 assert.Equal(t, "three", doc.Get("number")) 548 ref = getSharedRef(t, inst, bazs, bazThreeID) 549 assert.Equal(t, &RevsTree{Rev: "1-ddd"}, ref.Revisions) 550 assert.Contains(t, ref.Infos, s.SID) 551 assert.Equal(t, 2, ref.Infos[s.SID].Rule) 552 doc = getDoc(t, inst, bazs, bazFourID) 553 assert.Equal(t, "1-eee", doc.Rev()) 554 assert.Equal(t, "four", doc.Get("number")) 555 ref = getSharedRef(t, inst, bazs, bazFourID) 556 assert.Equal(t, &RevsTree{Rev: "1-eee"}, ref.Revisions) 557 assert.Contains(t, ref.Infos, s.SID) 558 assert.Equal(t, 2, ref.Infos[s.SID].Rule) 559 560 // And a mix of all cases 561 fooFiveID := uuidv7() 562 barSixID := uuidv7() 563 barSevenID := uuidv7() 564 barEightID := uuidv7() 565 barEightRev := createDoc(t, inst, bars, barEightID, map[string]interface{}{"hello": "world", "number": "8"}).Rev() 566 payload = DocsByDoctype{ 567 foos: DocsList{ 568 { 569 "_id": fooOneID, 570 "_rev": "3-fab", 571 "_revisions": map[string]interface{}{ 572 "start": float64(3), 573 "ids": []interface{}{"fab", "def", "abc"}, 574 }, 575 "hello": "world", 576 "number": "one ter", 577 }, 578 { 579 "_id": fooFiveID, 580 "_rev": "1-aab", 581 "_revisions": map[string]interface{}{ 582 "start": float64(1), 583 "ids": []interface{}{"aab"}, 584 }, 585 "hello": "world", 586 "number": "five", 587 }, 588 }, 589 bars: DocsList{ 590 { 591 "_id": barSixID, 592 "_rev": "1-aac", 593 "_revisions": map[string]interface{}{ 594 "start": float64(1), 595 "ids": []interface{}{"aac"}, 596 }, 597 "hello": "world", 598 "number": "six", 599 }, 600 { 601 "_id": barSevenID, 602 "_rev": "1-bad", 603 "_revisions": map[string]interface{}{ 604 "start": float64(1), 605 "ids": []interface{}{"bad"}, 606 }, 607 "not": "shared", 608 "number": "seven", 609 }, 610 { 611 "_id": barEightID, 612 "_rev": barEightRev, 613 "_revisions": map[string]interface{}{ 614 "start": float64(1), 615 "ids": []interface{}{strings.Replace(barEightRev, "1-", "", 1)}, 616 }, 617 "hello": "world", 618 "number": "8 bis", 619 }, 620 { 621 "_id": barZeroID, 622 "_rev": "2-222", 623 "_revisions": map[string]interface{}{ 624 "start": float64(2), 625 "ids": []interface{}{"222", "111"}, 626 }, 627 "hello": "world", 628 "number": "zero bis", 629 }, 630 { 631 "_id": barTwoID, 632 "_rev": "3-daa", 633 "_revisions": map[string]interface{}{ 634 "start": float64(3), 635 "ids": []interface{}{"daa", "caa"}, 636 }, 637 "hello": "world", 638 "number": "two bis", 639 }, 640 }, 641 bazs: DocsList{ 642 { 643 "_id": bazThreeID, 644 "_rev": "3-ddf", 645 "_revisions": map[string]interface{}{ 646 "start": float64(3), 647 "ids": []interface{}{"ddf", "dde", "ddd"}, 648 }, 649 "hello": "world", 650 "number": "three bis", 651 }, 652 }, 653 } 654 err = s.ApplyBulkDocs(inst, payload) 655 assert.NoError(t, err) 656 nbShared += 2 // fooFiveID and barSixID 657 assertNbSharedRef(t, inst, nbShared) 658 doc = getDoc(t, inst, foos, fooOneID) 659 assert.Equal(t, "3-fab", doc.Rev()) 660 assert.Equal(t, "one ter", doc.Get("number")) 661 ref = getSharedRef(t, inst, foos, fooOneID) 662 expected = &RevsTree{Rev: "1-abc"} 663 expected.Add("2-def") 664 expected.Add("3-fab") 665 assert.Equal(t, expected, ref.Revisions) 666 assert.Contains(t, ref.Infos, s.SID) 667 assert.Equal(t, 0, ref.Infos[s.SID].Rule) 668 doc = getDoc(t, inst, foos, fooFiveID) 669 assert.Equal(t, "1-aab", doc.Rev()) 670 assert.Equal(t, "five", doc.Get("number")) 671 ref = getSharedRef(t, inst, foos, fooFiveID) 672 assert.Equal(t, &RevsTree{Rev: "1-aab"}, ref.Revisions) 673 assert.Contains(t, ref.Infos, s.SID) 674 assert.Equal(t, 0, ref.Infos[s.SID].Rule) 675 doc = getDoc(t, inst, bazs, bazThreeID) 676 assert.Equal(t, "3-ddf", doc.Rev()) 677 assert.Equal(t, "three bis", doc.Get("number")) 678 ref = getSharedRef(t, inst, bazs, bazThreeID) 679 expected = &RevsTree{Rev: "1-ddd"} 680 expected.Add("2-dde") 681 expected.Add("3-ddf") 682 assert.Equal(t, expected, ref.Revisions) 683 assert.Contains(t, ref.Infos, s.SID) 684 assert.Equal(t, 2, ref.Infos[s.SID].Rule) 685 doc = getDoc(t, inst, bars, barSixID) 686 assert.Equal(t, "1-aac", doc.Rev()) 687 assert.Equal(t, "six", doc.Get("number")) 688 ref = getSharedRef(t, inst, bars, barSixID) 689 assert.Equal(t, &RevsTree{Rev: "1-aac"}, ref.Revisions) 690 assert.Contains(t, ref.Infos, s.SID) 691 assert.Equal(t, 1, ref.Infos[s.SID].Rule) 692 doc = getDoc(t, inst, bars, barTwoID) 693 assert.Equal(t, "3-daa", doc.Rev()) 694 assert.Equal(t, "two bis", doc.Get("number")) 695 ref = getSharedRef(t, inst, bars, barTwoID) 696 expected = &RevsTree{Rev: "2-caa"} 697 expected.Add("3-daa") 698 assert.Equal(t, expected, ref.Revisions) 699 assert.Contains(t, ref.Infos, s.SID) 700 assert.Equal(t, 1, ref.Infos[s.SID].Rule) 701 // New document rejected because it doesn't match the rules 702 assertNoDoc(t, inst, bars, barSevenID) 703 // Existing document with no shared reference 704 doc = getDoc(t, inst, bars, barEightID) 705 assert.Equal(t, barEightRev, doc.Rev()) 706 assert.Equal(t, "8", doc.Get("number")) 707 // Existing document with a shared reference, but not for the good sharing 708 doc = getDoc(t, inst, bars, barZeroID) 709 assert.Equal(t, "1-111", doc.Rev()) 710 assert.Equal(t, "zero", doc.Get("number")) 711 }) 712 } 713 714 func uuidv7() string { 715 return uuid.Must(uuid.NewV7()).String() 716 } 717 718 func createASharedRef(t *testing.T, inst *instance.Instance, id string) { 719 ref := SharedRef{ 720 SID: testDoctype + "/" + uuidv7(), 721 Revisions: &RevsTree{Rev: "1-aaa"}, 722 Infos: map[string]SharedInfo{ 723 id: {Rule: 0}, 724 }, 725 } 726 err := couchdb.CreateNamedDocWithDB(inst, &ref) 727 assert.NoError(t, err) 728 } 729 730 func createDoc(t *testing.T, inst *instance.Instance, doctype, id string, attrs map[string]interface{}) *couchdb.JSONDoc { 731 attrs["_id"] = id 732 doc := couchdb.JSONDoc{ 733 M: attrs, 734 Type: doctype, 735 } 736 err := couchdb.CreateNamedDocWithDB(inst, &doc) 737 assert.NoError(t, err) 738 return &doc 739 } 740 741 func updateDoc(t *testing.T, inst *instance.Instance, doctype, id, rev string, attrs map[string]interface{}) *couchdb.JSONDoc { 742 doc := couchdb.JSONDoc{ 743 M: attrs, 744 Type: doctype, 745 } 746 doc.SetID(id) 747 doc.SetRev(rev) 748 err := couchdb.UpdateDoc(inst, &doc) 749 assert.NoError(t, err) 750 return &doc 751 } 752 753 func getSharedRef(t *testing.T, inst *instance.Instance, doctype, id string) *SharedRef { 754 var ref SharedRef 755 err := couchdb.GetDoc(inst, consts.Shared, doctype+"/"+id, &ref) 756 assert.NoError(t, err) 757 return &ref 758 } 759 760 func assertNbSharedRef(t *testing.T, inst *instance.Instance, expected int) { 761 nb, err := couchdb.CountAllDocs(inst, consts.Shared) 762 if err != nil { 763 time.Sleep(1 * time.Second) 764 nb, err = couchdb.CountAllDocs(inst, consts.Shared) 765 } 766 assert.NoError(t, err) 767 assert.Equal(t, expected, nb) 768 } 769 770 func createSharedRef(t *testing.T, inst *instance.Instance, sharingID, sid string, revisions []string) *SharedRef { 771 tree := &RevsTree{Rev: revisions[0]} 772 sub := tree 773 for _, rev := range revisions[1:] { 774 sub.Branches = []RevsTree{ 775 {Rev: rev}, 776 } 777 sub = &sub.Branches[0] 778 } 779 ref := SharedRef{ 780 SID: sid, 781 Revisions: tree, 782 Infos: map[string]SharedInfo{ 783 sharingID: {Rule: 0}, 784 }, 785 } 786 err := couchdb.CreateNamedDocWithDB(inst, &ref) 787 assert.NoError(t, err) 788 return &ref 789 } 790 791 func appendRevisionToSharedRef(t *testing.T, inst *instance.Instance, ref *SharedRef, revision string) { 792 ref.Revisions.Add(revision) 793 err := couchdb.UpdateDoc(inst, ref) 794 assert.NoError(t, err) 795 } 796 797 func stripGenerations(revs ...string) []interface{} { 798 res := make([]interface{}, len(revs)) 799 for i, rev := range revs { 800 parts := strings.SplitN(rev, "-", 2) 801 res[i] = parts[1] 802 } 803 return res 804 } 805 806 func getDoc(t *testing.T, inst *instance.Instance, doctype, id string) *couchdb.JSONDoc { 807 var doc couchdb.JSONDoc 808 err := couchdb.GetDoc(inst, doctype, id, &doc) 809 assert.NoError(t, err) 810 return &doc 811 } 812 813 func assertNoDoc(t *testing.T, inst *instance.Instance, doctype, id string) { 814 var doc couchdb.JSONDoc 815 err := couchdb.GetDoc(inst, doctype, id, &doc) 816 assert.Error(t, err) 817 }