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  }